@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,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns the raw LuaLS type-expression tokens `parse-luals.ts` preserves verbatim
|
|
3
|
+
* (`integer`, `string?`, `fun(self):number`, `table<K,V>`, `druid.component`,
|
|
4
|
+
* `vmath.vector3`) into TypeScript type strings. Pure and deterministic: mapping a
|
|
5
|
+
* token has no I/O and depends only on the token and the supplied `MapContext`.
|
|
6
|
+
*
|
|
7
|
+
* It mirrors two disciplines from the ts-defold front-end
|
|
8
|
+
* (`sync-library-types.ts`): a `vmath.*`-namespaced token with no rename is a hard
|
|
9
|
+
* error (a missing core mapping must surface, never lower to a silent `any`); any
|
|
10
|
+
* other unresolved reference lowers to `unknown` and is recorded so a fidelity
|
|
11
|
+
* report can show the gap. Scope is mapping only — no declaration text, no
|
|
12
|
+
* identifier sanitization; a known class reference resolves to its model name
|
|
13
|
+
* verbatim and the emitter sanitizes it later.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { CORE_TYPE_RENAMES } from "./sync-library-types";
|
|
17
|
+
|
|
18
|
+
export interface MapContext {
|
|
19
|
+
knownNames: ReadonlySet<string>;
|
|
20
|
+
typeRenames: Readonly<Record<string, string>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MapResult {
|
|
24
|
+
ts: string;
|
|
25
|
+
unknowns: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A child `MapContext` with each generic parameter `name` added as an identity
|
|
30
|
+
* rename and a known name, so a bare `T` maps to `T` instead of lowering to
|
|
31
|
+
* `unknown`. Returns the same ctx when there are no generics. Shared by the emitter
|
|
32
|
+
* (declaration text) and the fidelity report (coverage) so both scope generics
|
|
33
|
+
* identically.
|
|
34
|
+
*/
|
|
35
|
+
export function scopeGenerics(ctx: MapContext, generics: readonly { name: string }[]): MapContext {
|
|
36
|
+
if (generics.length === 0) return ctx;
|
|
37
|
+
const knownNames = new Set(ctx.knownNames);
|
|
38
|
+
const typeRenames = { ...ctx.typeRenames };
|
|
39
|
+
for (const generic of generics) {
|
|
40
|
+
knownNames.add(generic.name);
|
|
41
|
+
typeRenames[generic.name] = generic.name;
|
|
42
|
+
}
|
|
43
|
+
return { knownNames, typeRenames };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SCALARS: Readonly<Record<string, string>> = {
|
|
47
|
+
integer: "number",
|
|
48
|
+
number: "number",
|
|
49
|
+
string: "string",
|
|
50
|
+
boolean: "boolean",
|
|
51
|
+
nil: "undefined",
|
|
52
|
+
any: "unknown",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Split `s` on every top-level occurrence of the single-character `sep`, honoring
|
|
57
|
+
* bracket depth and double-quoted string literals so a separator nested inside
|
|
58
|
+
* `<...>`, `(...)`, `[...]`, `{...}`, or a `"..."` literal does not split.
|
|
59
|
+
*/
|
|
60
|
+
function splitTopLevel(s: string, sep: string): string[] {
|
|
61
|
+
const parts: string[] = [];
|
|
62
|
+
let depth = 0;
|
|
63
|
+
let inQuote = false;
|
|
64
|
+
let start = 0;
|
|
65
|
+
for (let i = 0; i < s.length; i++) {
|
|
66
|
+
const c = s[i];
|
|
67
|
+
if (inQuote) {
|
|
68
|
+
if (c === '"') inQuote = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === '"') inQuote = true;
|
|
72
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
73
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
74
|
+
else if (depth === 0 && c === sep) {
|
|
75
|
+
parts.push(s.slice(start, i));
|
|
76
|
+
start = i + 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
parts.push(s.slice(start));
|
|
80
|
+
return parts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Index of the matching close bracket for the opener at `open`, or -1 if unbalanced. */
|
|
84
|
+
function matchBracket(s: string, open: number): number {
|
|
85
|
+
const closers: Record<string, string> = { "<": ">", "(": ")", "[": "]", "{": "}" };
|
|
86
|
+
const want = closers[s[open] as string];
|
|
87
|
+
let depth = 0;
|
|
88
|
+
let inQuote = false;
|
|
89
|
+
for (let i = open; i < s.length; i++) {
|
|
90
|
+
const c = s[i];
|
|
91
|
+
if (inQuote) {
|
|
92
|
+
if (c === '"') inQuote = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === '"') inQuote = true;
|
|
96
|
+
else if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
97
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") {
|
|
98
|
+
depth--;
|
|
99
|
+
if (depth === 0) return c === want ? i : -1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return -1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True when a top-level `=>` (an arrow function type) appears in a mapped result. */
|
|
106
|
+
function hasTopLevelArrow(tsExpr: string): boolean {
|
|
107
|
+
let depth = 0;
|
|
108
|
+
for (let i = 0; i + 1 < tsExpr.length; i++) {
|
|
109
|
+
const c = tsExpr[i];
|
|
110
|
+
if (c === "<" || c === "(" || c === "[" || c === "{") depth++;
|
|
111
|
+
else if (c === ">" || c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
112
|
+
else if (depth === 0 && c === "=" && tsExpr[i + 1] === ">") return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A union member needs parentheses when it is itself a function type. */
|
|
118
|
+
function wrapForUnion(tsExpr: string): string {
|
|
119
|
+
return hasTopLevelArrow(tsExpr) ? `(${tsExpr})` : tsExpr;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** An array element needs parentheses when it is a union, a function, or an object. */
|
|
123
|
+
function needsArrayParens(tsExpr: string): boolean {
|
|
124
|
+
return (
|
|
125
|
+
splitTopLevel(tsExpr, "|").length > 1 || hasTopLevelArrow(tsExpr) || tsExpr.startsWith("{")
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mapFunction(token: string, ctx: MapContext, unknowns: string[]): string {
|
|
130
|
+
const open = token.indexOf("(");
|
|
131
|
+
const close = matchBracket(token, open);
|
|
132
|
+
const paramsStr = token.slice(open + 1, close).trim();
|
|
133
|
+
const afterClose = token.slice(close + 1).trim();
|
|
134
|
+
|
|
135
|
+
const params = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
|
|
136
|
+
const paramList = params
|
|
137
|
+
.map((raw) => raw.trim())
|
|
138
|
+
.map((part) => {
|
|
139
|
+
if (part.startsWith("...")) {
|
|
140
|
+
const after = part.slice(3).trim();
|
|
141
|
+
let element: string;
|
|
142
|
+
if (after.startsWith(":")) {
|
|
143
|
+
element = mapToken(after.slice(1).trim(), ctx, unknowns);
|
|
144
|
+
} else {
|
|
145
|
+
element = "unknown";
|
|
146
|
+
unknowns.push("...");
|
|
147
|
+
}
|
|
148
|
+
return `...args: ${needsArrayParens(element) ? `(${element})[]` : `${element}[]`}`;
|
|
149
|
+
}
|
|
150
|
+
const colon = splitTopLevel(part, ":");
|
|
151
|
+
if (colon.length < 2) {
|
|
152
|
+
// Untyped param (`self`, `_`, `ctx`): a recorded gap, not a silent `any`.
|
|
153
|
+
unknowns.push(part);
|
|
154
|
+
return `${part}: unknown`;
|
|
155
|
+
}
|
|
156
|
+
const name = colon[0]?.trim() ?? "";
|
|
157
|
+
const typeExpr = colon.slice(1).join(":").trim();
|
|
158
|
+
const mapped = mapToken(typeExpr, ctx, unknowns);
|
|
159
|
+
return `${name}: ${mapped}`;
|
|
160
|
+
})
|
|
161
|
+
.join(", ");
|
|
162
|
+
|
|
163
|
+
let ret = "void";
|
|
164
|
+
if (afterClose.startsWith(":")) {
|
|
165
|
+
const retStr = afterClose.slice(1).trim();
|
|
166
|
+
const retTokens = retStr === "" ? [] : splitTopLevel(retStr, ",").map((r) => r.trim());
|
|
167
|
+
if (retTokens.length === 1) {
|
|
168
|
+
ret = mapToken(retTokens[0] as string, ctx, unknowns);
|
|
169
|
+
} else if (retTokens.length > 1) {
|
|
170
|
+
const inner = retTokens.map((r) => mapToken(r, ctx, unknowns)).join(", ");
|
|
171
|
+
ret = `LuaMultiReturn<[${inner}]>`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return `(${paramList}) => ${ret}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mapObject(token: string, ctx: MapContext, unknowns: string[]): string {
|
|
178
|
+
const inner = token.slice(1, -1).trim();
|
|
179
|
+
if (inner === "") return "{}";
|
|
180
|
+
const entries = splitTopLevel(inner, ",")
|
|
181
|
+
.map((raw) => raw.trim())
|
|
182
|
+
.filter((part) => part.length > 0)
|
|
183
|
+
.map((part) => {
|
|
184
|
+
const colon = splitTopLevel(part, ":");
|
|
185
|
+
const key = colon[0]?.trim() ?? "";
|
|
186
|
+
const typeExpr = colon.slice(1).join(":").trim();
|
|
187
|
+
return `${key}: ${mapToken(typeExpr, ctx, unknowns)}`;
|
|
188
|
+
});
|
|
189
|
+
return `{ ${entries.join("; ")} }`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
|
|
193
|
+
let token = raw.trim();
|
|
194
|
+
|
|
195
|
+
// Strip a redundant pair of outer parentheses (LuaLS grouping) so `(a | b)[]`
|
|
196
|
+
// reaches the union handler rather than falling through to a reference lookup.
|
|
197
|
+
while (token.startsWith("(") && matchBracket(token, 0) === token.length - 1) {
|
|
198
|
+
token = token.slice(1, -1).trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (token === "") return "unknown";
|
|
202
|
+
|
|
203
|
+
// Optional suffix.
|
|
204
|
+
if (token.length > 1 && token.endsWith("?")) {
|
|
205
|
+
const base = mapToken(token.slice(0, -1), ctx, unknowns);
|
|
206
|
+
const members = splitTopLevel(base, "|").map((m) => m.trim());
|
|
207
|
+
return members.includes("undefined") ? base : `${base} | undefined`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A `fun(...)` whose return follows the `)` keeps its return-type `|` inside the
|
|
211
|
+
// function; splitting the union first would cut `fun(): a|b` into `(fun) | b`.
|
|
212
|
+
// `fun()|nil` (a `|` right after the `)`) falls through to the union split.
|
|
213
|
+
if (/^fun\s*\(/.test(token)) {
|
|
214
|
+
const close = matchBracket(token, token.indexOf("("));
|
|
215
|
+
const afterClose = close === -1 ? "" : token.slice(close + 1).trim();
|
|
216
|
+
if (close !== -1 && afterClose.startsWith(":")) {
|
|
217
|
+
return mapFunction(token, ctx, unknowns);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Top-level union.
|
|
222
|
+
const unionParts = splitTopLevel(token, "|");
|
|
223
|
+
if (unionParts.length > 1) {
|
|
224
|
+
return unionParts.map((p) => wrapForUnion(mapToken(p.trim(), ctx, unknowns))).join(" | ");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Trailing array.
|
|
228
|
+
if (token.endsWith("[]")) {
|
|
229
|
+
const element = mapToken(token.slice(0, -2), ctx, unknowns);
|
|
230
|
+
return needsArrayParens(element) ? `(${element})[]` : `${element}[]`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Function.
|
|
234
|
+
if (/^fun\s*\(/.test(token)) return mapFunction(token, ctx, unknowns);
|
|
235
|
+
|
|
236
|
+
// Table.
|
|
237
|
+
if (token === "table") return "LuaTable";
|
|
238
|
+
if (token.startsWith("table<") && token.endsWith(">")) {
|
|
239
|
+
const args = splitTopLevel(token.slice(6, -1), ",").map((a) =>
|
|
240
|
+
mapToken(a.trim(), ctx, unknowns),
|
|
241
|
+
);
|
|
242
|
+
return `LuaTable<${args.join(", ")}>`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Inline object.
|
|
246
|
+
if (token.startsWith("{") && token.endsWith("}")) return mapObject(token, ctx, unknowns);
|
|
247
|
+
|
|
248
|
+
// String literal — passthrough.
|
|
249
|
+
if (token.startsWith('"') && token.endsWith('"')) return token;
|
|
250
|
+
|
|
251
|
+
// Scalars.
|
|
252
|
+
const scalar = SCALARS[token];
|
|
253
|
+
if (scalar !== undefined) return scalar;
|
|
254
|
+
|
|
255
|
+
// Reference-token precedence: per-target rename, core rename, loud-fail on an
|
|
256
|
+
// unmapped `vmath.*`, known model reference verbatim, else recorded `unknown`.
|
|
257
|
+
const override = ctx.typeRenames[token];
|
|
258
|
+
if (override !== undefined) return override;
|
|
259
|
+
const core = CORE_TYPE_RENAMES[token];
|
|
260
|
+
if (core !== undefined) return core;
|
|
261
|
+
if (token.startsWith("vmath.")) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`luals type mapper: unmapped Defold core token "${token}" - extend CORE_TYPE_RENAMES or the target's typeRenames.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (ctx.knownNames.has(token)) return token;
|
|
267
|
+
unknowns.push(token);
|
|
268
|
+
return "unknown";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Map one raw LuaLS type token to a TypeScript type string. */
|
|
272
|
+
export function mapLualsType(token: string, ctx: MapContext): MapResult {
|
|
273
|
+
const unknowns: string[] = [];
|
|
274
|
+
const ts = mapToken(token, ctx, unknowns);
|
|
275
|
+
return { ts, unknowns };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* When `types` is exactly one `fun(self: <selfTypeName>, ...)` token — optionally
|
|
280
|
+
* unioned with `nil` — whose first parameter is `self` typed as the enclosing
|
|
281
|
+
* interface's own model name, return the function's raw return tokens (an empty
|
|
282
|
+
* array for a `void`/no-return hook). Returns `null` for every other shape: a data
|
|
283
|
+
* field, a non-`fun` type, an untyped `self`, or a `self` typed as a *different*
|
|
284
|
+
* interface. Reuses the same bracket-aware split/match as the mapper so nested
|
|
285
|
+
* commas and colons inside a param type never mis-split.
|
|
286
|
+
*
|
|
287
|
+
* The `fun` is isolated through its params `)` before the return is read, so a
|
|
288
|
+
* return union or nullable return (which also sits at bracket depth 0, after the
|
|
289
|
+
* `)`) survives instead of being mis-split as an outer `|nil` — the same
|
|
290
|
+
* ordering rule as the mapper's `fun(...)` return-union handling.
|
|
291
|
+
*/
|
|
292
|
+
export function matchSelfHookField(
|
|
293
|
+
types: readonly string[],
|
|
294
|
+
selfTypeName: string,
|
|
295
|
+
): string[] | null {
|
|
296
|
+
if (types.length !== 1) return null;
|
|
297
|
+
const raw = (types[0] as string).trim();
|
|
298
|
+
const members = splitTopLevel(raw, "|").map((member) => member.trim());
|
|
299
|
+
const funIndex = members.findIndex((member) => /^fun\s*\(/.test(member));
|
|
300
|
+
if (funIndex === -1) return null;
|
|
301
|
+
// Anything unioned before the fun may only be a bare outer nullable.
|
|
302
|
+
if (members.slice(0, funIndex).some((member) => member !== "nil")) return null;
|
|
303
|
+
// Rejoin from the fun rightward so a return union or nullable stays whole.
|
|
304
|
+
const fun = members.slice(funIndex).join("|");
|
|
305
|
+
const open = fun.indexOf("(");
|
|
306
|
+
const close = matchBracket(fun, open);
|
|
307
|
+
if (close === -1) return null;
|
|
308
|
+
const paramsStr = fun.slice(open + 1, close).trim();
|
|
309
|
+
const params = paramsStr === "" ? [] : splitTopLevel(paramsStr, ",");
|
|
310
|
+
const first = (params[0]?.trim() ?? "").length > 0 ? splitTopLevel(params[0] as string, ":") : [];
|
|
311
|
+
if (first.length < 2) return null;
|
|
312
|
+
if ((first[0] as string).trim() !== "self") return null;
|
|
313
|
+
if (first.slice(1).join(":").trim() !== selfTypeName) return null;
|
|
314
|
+
const afterClose = fun.slice(close + 1).trim();
|
|
315
|
+
if (afterClose === "") return [];
|
|
316
|
+
if (afterClose.startsWith(":")) {
|
|
317
|
+
const retStr = afterClose.slice(1).trim();
|
|
318
|
+
return retStr === "" ? [] : splitTopLevel(retStr, ",").map((token) => token.trim());
|
|
319
|
+
}
|
|
320
|
+
// Anything else after the params `)` must be an outer `| nil` (whole hook optional).
|
|
321
|
+
const outer = splitTopLevel(afterClose, "|").map((member) => member.trim());
|
|
322
|
+
return outer.every((member) => member === "" || member === "nil") ? [] : null;
|
|
323
|
+
}
|