@defold-typescript/library-types 0.20.7 → 0.21.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/decore.json +1883 -0
- package/api-doc/druid.json +11818 -0
- package/generated/decore.d.ts +228 -0
- package/generated/druid.d.ts +2067 -0
- package/luals-targets.json +28 -0
- package/package.json +8 -3
- package/scripts/__snapshots__/emit-library-dts.test.ts.snap +22 -0
- package/scripts/__snapshots__/parse-luals.test.ts.snap +15780 -0
- package/scripts/emit-library-dts.ts +252 -0
- package/scripts/lower-api-doc.ts +115 -0
- package/scripts/luals-fidelity.ts +123 -0
- package/scripts/map-luals-types.ts +323 -0
- package/scripts/parse-luals.ts +432 -0
- package/scripts/sync-luals-types.ts +303 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line-oriented reader for the LuaLS `---@` annotation dialect that druid-style
|
|
3
|
+
* pure-Lua libraries ship in place of a `.d.ts`. It populates a `LibraryModel`, a
|
|
4
|
+
* richer OOP shape than the flat `ApiModule` (`packages/types/src/api-doc.ts`):
|
|
5
|
+
* interfaces with methods/fields/generics/extends, aliases, and free module
|
|
6
|
+
* functions. Naming mirrors the flat model where it fits (`types: string[]`,
|
|
7
|
+
* `brief`, `isOptional`, `doc`) so the two read alike.
|
|
8
|
+
*
|
|
9
|
+
* Scope is parse-only: every LuaLS type expression is preserved as a raw token
|
|
10
|
+
* string, verbatim (`integer`, `string?`, `fun(self):number`, `table<K,V>`,
|
|
11
|
+
* `"a" | "b"`). Mapping those tokens to TypeScript is the next goal; this reader
|
|
12
|
+
* never rewrites, splits, or normalizes a type toward TS.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface LibraryModel {
|
|
16
|
+
interfaces: LibraryInterface[];
|
|
17
|
+
aliases: LibraryAlias[];
|
|
18
|
+
moduleFunctions: LibraryMethod[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LibraryInterface {
|
|
22
|
+
name: string;
|
|
23
|
+
extends?: string;
|
|
24
|
+
generics: LibraryGeneric[];
|
|
25
|
+
fields: LibraryField[];
|
|
26
|
+
methods: LibraryMethod[];
|
|
27
|
+
brief: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LibraryMethod {
|
|
31
|
+
name: string;
|
|
32
|
+
brief: string;
|
|
33
|
+
generics: LibraryGeneric[];
|
|
34
|
+
params: LibraryParam[];
|
|
35
|
+
returns: LibraryParam[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LibraryParam {
|
|
39
|
+
name: string;
|
|
40
|
+
types: string[];
|
|
41
|
+
doc: string;
|
|
42
|
+
isOptional: boolean;
|
|
43
|
+
isVararg: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type LibraryFieldVisibility = "public" | "protected" | "private" | "package";
|
|
47
|
+
|
|
48
|
+
export interface LibraryField {
|
|
49
|
+
name: string;
|
|
50
|
+
types: string[];
|
|
51
|
+
doc: string;
|
|
52
|
+
isOptional: boolean;
|
|
53
|
+
visibility?: LibraryFieldVisibility;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LibraryGeneric {
|
|
57
|
+
name: string;
|
|
58
|
+
constraint?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LibraryAlias {
|
|
62
|
+
name: string;
|
|
63
|
+
types: string[];
|
|
64
|
+
doc: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface Pending {
|
|
68
|
+
doc: string[];
|
|
69
|
+
params: LibraryParam[];
|
|
70
|
+
returns: LibraryParam[];
|
|
71
|
+
generics: LibraryGeneric[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const emptyPending = (): Pending => ({ doc: [], params: [], returns: [], generics: [] });
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read a single raw type token from the head of `rest`, honoring bracket depth so
|
|
78
|
+
* an inner space (`table<string, any>`, `fun(a, b): c`) does not end the token. The
|
|
79
|
+
* token ends at the first top-level whitespace, except that a space right after a
|
|
80
|
+
* top-level `:` or `,` continues the token — so a spaced `fun(text_id: string):
|
|
81
|
+
* string` return arrow and a multi-return `fun(): number, string` separator are kept
|
|
82
|
+
* whole rather than truncated at the `):`/`,`. Returns the token and the trailing
|
|
83
|
+
* remainder (the human description). Never rewrites the token toward TS. The only
|
|
84
|
+
* inputs carrying a top-level `:`/`,` are `fun(...)` type expressions, so plain
|
|
85
|
+
* types, unions, and descriptions are unaffected.
|
|
86
|
+
*/
|
|
87
|
+
function readTypeToken(rest: string): { type: string; rest: string } {
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let lastNonSpace = "";
|
|
90
|
+
let i = 0;
|
|
91
|
+
for (; i < rest.length; i++) {
|
|
92
|
+
const c = rest[i];
|
|
93
|
+
if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
94
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
95
|
+
else if ((c === " " || c === "\t") && depth === 0) {
|
|
96
|
+
if (lastNonSpace !== ":" && lastNonSpace !== ",") break;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c !== undefined && c !== " " && c !== "\t") lastNonSpace = c;
|
|
100
|
+
}
|
|
101
|
+
return { type: rest.slice(0, i), rest: rest.slice(i).trim() };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A bare lowercase identifier — the shape druid uses for an optional `@return` name. */
|
|
105
|
+
const RETURN_NAME = /^[a-z_][A-Za-z0-9_]*$/;
|
|
106
|
+
|
|
107
|
+
function parseParam(rest: string): LibraryParam {
|
|
108
|
+
const spaceAt = rest.search(/\s/);
|
|
109
|
+
const rawName = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
|
|
110
|
+
const afterName = spaceAt === -1 ? "" : rest.slice(spaceAt).trim();
|
|
111
|
+
const isVararg = rawName === "...";
|
|
112
|
+
const isOptional = !isVararg && rawName.endsWith("?");
|
|
113
|
+
const name = isOptional ? rawName.slice(0, -1) : rawName;
|
|
114
|
+
const { type, rest: doc } = readTypeToken(afterName);
|
|
115
|
+
return { name, types: type ? [type] : [], doc, isOptional, isVararg };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseReturn(rest: string): LibraryParam {
|
|
119
|
+
const { type, rest: afterType } = readTypeToken(rest);
|
|
120
|
+
const spaceAt = afterType.search(/\s/);
|
|
121
|
+
const head = spaceAt === -1 ? afterType : afterType.slice(0, spaceAt);
|
|
122
|
+
let name = "";
|
|
123
|
+
let doc = afterType;
|
|
124
|
+
if (head && RETURN_NAME.test(head)) {
|
|
125
|
+
name = head;
|
|
126
|
+
doc = spaceAt === -1 ? "" : afterType.slice(spaceAt).trim();
|
|
127
|
+
}
|
|
128
|
+
return { name, types: type ? [type] : [], doc, isOptional: false, isVararg: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const VISIBILITY_KEYWORDS = new Set<LibraryFieldVisibility>([
|
|
132
|
+
"public",
|
|
133
|
+
"protected",
|
|
134
|
+
"private",
|
|
135
|
+
"package",
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
function parseField(rest: string): LibraryField {
|
|
139
|
+
// LuaLS grammar is `---@field [scope] <name> <type> [description]`. Strip a leading
|
|
140
|
+
// visibility keyword only when a further token follows it — a lone `---@field private`
|
|
141
|
+
// is a field literally named `private`, matching LuaLS's own resolution.
|
|
142
|
+
let body = rest;
|
|
143
|
+
let visibility: LibraryFieldVisibility | undefined;
|
|
144
|
+
const firstSpace = body.search(/\s/);
|
|
145
|
+
if (firstSpace !== -1) {
|
|
146
|
+
const first = body.slice(0, firstSpace);
|
|
147
|
+
if (VISIBILITY_KEYWORDS.has(first as LibraryFieldVisibility)) {
|
|
148
|
+
visibility = first as LibraryFieldVisibility;
|
|
149
|
+
body = body.slice(firstSpace).trim();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const spaceAt = body.search(/\s/);
|
|
153
|
+
const rawName = spaceAt === -1 ? body : body.slice(0, spaceAt);
|
|
154
|
+
const afterName = spaceAt === -1 ? "" : body.slice(spaceAt).trim();
|
|
155
|
+
const isOptional = rawName.endsWith("?");
|
|
156
|
+
const name = isOptional ? rawName.slice(0, -1) : rawName;
|
|
157
|
+
const { type, rest: doc } = readTypeToken(afterName);
|
|
158
|
+
return {
|
|
159
|
+
name,
|
|
160
|
+
types: type ? [type] : [],
|
|
161
|
+
doc,
|
|
162
|
+
isOptional,
|
|
163
|
+
...(visibility ? { visibility } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseVararg(rest: string): LibraryParam {
|
|
168
|
+
const { type, rest: doc } = readTypeToken(rest);
|
|
169
|
+
return { name: "...", types: type ? [type] : [], doc, isOptional: false, isVararg: true };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseGenerics(rest: string): LibraryGeneric[] {
|
|
173
|
+
return rest
|
|
174
|
+
.split(",")
|
|
175
|
+
.map((part) => part.trim())
|
|
176
|
+
.filter((part) => part.length > 0)
|
|
177
|
+
.map((part) => {
|
|
178
|
+
const colon = part.indexOf(":");
|
|
179
|
+
if (colon === -1) return { name: part.trim() };
|
|
180
|
+
return { name: part.slice(0, colon).trim(), constraint: part.slice(colon + 1).trim() };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Parse a `@class Name[ : parent]` head. The parent is kept as a single raw token. */
|
|
185
|
+
function parseClassHead(rest: string): { name: string; extends?: string } {
|
|
186
|
+
const colon = rest.indexOf(":");
|
|
187
|
+
if (colon === -1) return { name: rest.trim() };
|
|
188
|
+
const parent = rest.slice(colon + 1).trim();
|
|
189
|
+
return { name: rest.slice(0, colon).trim(), ...(parent ? { extends: parent } : {}) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface FunctionDecl {
|
|
193
|
+
kind: "method" | "module";
|
|
194
|
+
receiver?: string;
|
|
195
|
+
name: string;
|
|
196
|
+
// A dotted module form (`function T.name`, `T.name = function`) is public module
|
|
197
|
+
// surface; a bare/`local` form (`function name`, `local function name`) is not.
|
|
198
|
+
qualified: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const FUNCTION_FORMS: {
|
|
202
|
+
re: RegExp;
|
|
203
|
+
kind: "method" | "module";
|
|
204
|
+
recv?: number;
|
|
205
|
+
name: number;
|
|
206
|
+
qualified?: boolean;
|
|
207
|
+
}[] = [
|
|
208
|
+
{ re: /^function\s+([A-Za-z_][\w.]*):([A-Za-z_]\w*)\s*\(/, kind: "method", recv: 1, name: 2 },
|
|
209
|
+
{
|
|
210
|
+
re: /^function\s+([A-Za-z_][\w.]*)\.([A-Za-z_]\w*)\s*\(/,
|
|
211
|
+
kind: "module",
|
|
212
|
+
name: 2,
|
|
213
|
+
qualified: true,
|
|
214
|
+
},
|
|
215
|
+
{ re: /^(?:local\s+)?function\s+([A-Za-z_]\w*)\s*\(/, kind: "module", name: 1, qualified: false },
|
|
216
|
+
{ re: /^([A-Za-z_][\w.]*):([A-Za-z_]\w*)\s*=\s*function\b/, kind: "method", recv: 1, name: 2 },
|
|
217
|
+
{
|
|
218
|
+
re: /^([A-Za-z_][\w.]*)\.([A-Za-z_]\w*)\s*=\s*function\b/,
|
|
219
|
+
kind: "module",
|
|
220
|
+
name: 2,
|
|
221
|
+
qualified: true,
|
|
222
|
+
},
|
|
223
|
+
{ re: /^([A-Za-z_]\w*)\s*=\s*function\b/, kind: "module", name: 1, qualified: false },
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
function parseFunctionDecl(line: string): FunctionDecl | null {
|
|
227
|
+
for (const form of FUNCTION_FORMS) {
|
|
228
|
+
const m = form.re.exec(line);
|
|
229
|
+
if (!m) continue;
|
|
230
|
+
const name = m[form.name] ?? "";
|
|
231
|
+
if (form.kind === "method") {
|
|
232
|
+
const receiver = form.recv ? m[form.recv] : undefined;
|
|
233
|
+
return { kind: "method", name, qualified: true, ...(receiver ? { receiver } : {}) };
|
|
234
|
+
}
|
|
235
|
+
return { kind: "module", name, qualified: form.qualified ?? false };
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const LOCAL_ASSIGN = /^local\s+([A-Za-z_]\w*)\s*=/;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Scan one LuaLS-annotated source into a `LibraryModel`. Only column-0 lines are
|
|
244
|
+
* recognized (module- and class-level declarations and their leading `---@` block);
|
|
245
|
+
* indented lines — in-body closures, `---@cast`/`---@type` narrowing — are opaque,
|
|
246
|
+
* so they neither create declarations nor pollute the pending block. Output order
|
|
247
|
+
* follows source order, making the result stable across repeated runs.
|
|
248
|
+
*
|
|
249
|
+
* Only dotted module forms (`function T.name`, `T.name = function`) count as module
|
|
250
|
+
* surface; a bare or `local function` is a private helper and is skipped. Limitation:
|
|
251
|
+
* a bare function later re-exported via `M.x = helper` is not recovered as surface.
|
|
252
|
+
*/
|
|
253
|
+
export function parseLualsSource(source: string): LibraryModel {
|
|
254
|
+
const interfaces: LibraryInterface[] = [];
|
|
255
|
+
const byName = new Map<string, LibraryInterface>();
|
|
256
|
+
const aliases: LibraryAlias[] = [];
|
|
257
|
+
const moduleFunctions: LibraryMethod[] = [];
|
|
258
|
+
const receiverBinding = new Map<string, string>();
|
|
259
|
+
|
|
260
|
+
let pending = emptyPending();
|
|
261
|
+
let openClass: LibraryInterface | null = null;
|
|
262
|
+
let lastOpenedClass: string | null = null;
|
|
263
|
+
|
|
264
|
+
const ensureInterface = (name: string): LibraryInterface => {
|
|
265
|
+
const existing = byName.get(name);
|
|
266
|
+
if (existing) return existing;
|
|
267
|
+
const created: LibraryInterface = {
|
|
268
|
+
name,
|
|
269
|
+
generics: [],
|
|
270
|
+
fields: [],
|
|
271
|
+
methods: [],
|
|
272
|
+
brief: "",
|
|
273
|
+
};
|
|
274
|
+
byName.set(name, created);
|
|
275
|
+
interfaces.push(created);
|
|
276
|
+
return created;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const methodFromPending = (name: string): LibraryMethod => ({
|
|
280
|
+
name,
|
|
281
|
+
brief: pending.doc.join("\n"),
|
|
282
|
+
generics: pending.generics,
|
|
283
|
+
params: pending.params,
|
|
284
|
+
returns: pending.returns,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
for (const raw of source.split("\n")) {
|
|
288
|
+
// Column-0 discipline: a line with leading whitespace is opaque to the scanner.
|
|
289
|
+
if (/^\s/.test(raw) || raw.length === 0) continue;
|
|
290
|
+
|
|
291
|
+
if (raw.startsWith("---@")) {
|
|
292
|
+
const tagMatch = /^---@([a-zA-Z]+)\s*(.*)$/.exec(raw);
|
|
293
|
+
if (!tagMatch) continue;
|
|
294
|
+
const tag = tagMatch[1];
|
|
295
|
+
const rest = (tagMatch[2] ?? "").trim();
|
|
296
|
+
switch (tag) {
|
|
297
|
+
case "class": {
|
|
298
|
+
const head = parseClassHead(rest);
|
|
299
|
+
const iface = ensureInterface(head.name);
|
|
300
|
+
if (head.extends) iface.extends = head.extends;
|
|
301
|
+
if (pending.doc.length > 0 && iface.brief === "") iface.brief = pending.doc.join("\n");
|
|
302
|
+
if (pending.generics.length > 0) iface.generics = pending.generics;
|
|
303
|
+
openClass = iface;
|
|
304
|
+
lastOpenedClass = head.name;
|
|
305
|
+
pending = emptyPending();
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "field": {
|
|
309
|
+
if (openClass) openClass.fields.push(parseField(rest));
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case "param": {
|
|
313
|
+
pending.params.push(parseParam(rest));
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "vararg": {
|
|
317
|
+
pending.params.push(parseVararg(rest));
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case "return": {
|
|
321
|
+
pending.returns.push(parseReturn(rest));
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case "generic": {
|
|
325
|
+
pending.generics.push(...parseGenerics(rest));
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case "alias": {
|
|
329
|
+
const spaceAt = rest.search(/\s/);
|
|
330
|
+
const name = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
|
|
331
|
+
const expr = spaceAt === -1 ? "" : rest.slice(spaceAt).trim();
|
|
332
|
+
aliases.push({ name, types: expr ? [expr] : [], doc: pending.doc.join("\n") });
|
|
333
|
+
pending = emptyPending();
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
// @private, @protected, @cast, @type, @diagnostic, @overload, ... — outside
|
|
338
|
+
// the Druid subset; recognized as a tag and skipped, never treated as doc.
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (raw.startsWith("---")) {
|
|
345
|
+
pending.doc.push(raw.slice(3).trim());
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const decl = parseFunctionDecl(raw);
|
|
350
|
+
if (decl) {
|
|
351
|
+
if (decl.kind === "method") {
|
|
352
|
+
const target = decl.receiver ? (receiverBinding.get(decl.receiver) ?? decl.receiver) : "";
|
|
353
|
+
ensureInterface(target).methods.push(methodFromPending(decl.name));
|
|
354
|
+
} else if (decl.qualified) {
|
|
355
|
+
moduleFunctions.push(methodFromPending(decl.name));
|
|
356
|
+
}
|
|
357
|
+
pending = emptyPending();
|
|
358
|
+
openClass = null;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const localAssign = LOCAL_ASSIGN.exec(raw);
|
|
363
|
+
if (localAssign) {
|
|
364
|
+
const variable = localAssign[1];
|
|
365
|
+
if (variable && lastOpenedClass) receiverBinding.set(variable, lastOpenedClass);
|
|
366
|
+
lastOpenedClass = null;
|
|
367
|
+
openClass = null;
|
|
368
|
+
pending = emptyPending();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { interfaces, aliases, moduleFunctions };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Fold several parsed models into one, merging interfaces by name (concatenating
|
|
377
|
+
* fields and methods, keeping the first non-empty `extends`/`brief`/`generics`) and
|
|
378
|
+
* concatenating aliases and module functions in argument order. Deterministic given
|
|
379
|
+
* a stable input order — the snapshot feeds it the fixture files sorted by path.
|
|
380
|
+
*/
|
|
381
|
+
export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
|
|
382
|
+
const interfaces: LibraryInterface[] = [];
|
|
383
|
+
const byName = new Map<string, LibraryInterface>();
|
|
384
|
+
const aliases: LibraryAlias[] = [];
|
|
385
|
+
const moduleFunctions: LibraryMethod[] = [];
|
|
386
|
+
|
|
387
|
+
for (const model of models) {
|
|
388
|
+
for (const iface of model.interfaces) {
|
|
389
|
+
const existing = byName.get(iface.name);
|
|
390
|
+
if (!existing) {
|
|
391
|
+
const copy: LibraryInterface = {
|
|
392
|
+
name: iface.name,
|
|
393
|
+
...(iface.extends ? { extends: iface.extends } : {}),
|
|
394
|
+
generics: [...iface.generics],
|
|
395
|
+
fields: [...iface.fields],
|
|
396
|
+
methods: [...iface.methods],
|
|
397
|
+
brief: iface.brief,
|
|
398
|
+
};
|
|
399
|
+
byName.set(iface.name, copy);
|
|
400
|
+
interfaces.push(copy);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
existing.fields.push(...iface.fields);
|
|
404
|
+
existing.methods.push(...iface.methods);
|
|
405
|
+
if (!existing.extends && iface.extends) existing.extends = iface.extends;
|
|
406
|
+
if (existing.brief === "" && iface.brief !== "") existing.brief = iface.brief;
|
|
407
|
+
if (existing.generics.length === 0 && iface.generics.length > 0) {
|
|
408
|
+
existing.generics = [...iface.generics];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
aliases.push(...model.aliases);
|
|
412
|
+
moduleFunctions.push(...model.moduleFunctions);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// A class split across fixtures (e.g. druid's curated + runtime `druid.logger`
|
|
416
|
+
// blocks) concatenates both field sets above, so the same field name can appear
|
|
417
|
+
// twice with conflicting signatures — an invalid declaration masked by
|
|
418
|
+
// `skipLibCheck`. Collapse to the first occurrence; methods stay untouched so
|
|
419
|
+
// overloaded module functions keep every signature.
|
|
420
|
+
for (const iface of interfaces) iface.fields = dedupeByName(iface.fields);
|
|
421
|
+
|
|
422
|
+
return { interfaces, aliases, moduleFunctions };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function dedupeByName<T extends { name: string }>(items: T[]): T[] {
|
|
426
|
+
const seen = new Set<string>();
|
|
427
|
+
return items.filter((item) => {
|
|
428
|
+
if (seen.has(item.name)) return false;
|
|
429
|
+
seen.add(item.name);
|
|
430
|
+
return true;
|
|
431
|
+
});
|
|
432
|
+
}
|