@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,303 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { emitLibraryDeclarations } from "./emit-library-dts";
|
|
4
|
+
import { lowerLibraryModel } from "./lower-api-doc";
|
|
5
|
+
import { buildFidelityReport, type FidelityReport } from "./luals-fidelity";
|
|
6
|
+
import { type LibraryModel, mergeLibraryModels, parseLualsSource } from "./parse-luals";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The LuaLS ingestion front-end pins its source per-entry: a druid-style library
|
|
10
|
+
* ships no `.d.ts`, only inline LuaLS `---@` annotations, and each such library
|
|
11
|
+
* lives in its own repo at its own tag. So every target carries its own
|
|
12
|
+
* `repo`/`ref`, unlike the ts-defold front-end's single shared `source`.
|
|
13
|
+
*/
|
|
14
|
+
export interface LualsTarget {
|
|
15
|
+
repo: string;
|
|
16
|
+
ref: string;
|
|
17
|
+
sourceGlobs: string[];
|
|
18
|
+
moduleId: string;
|
|
19
|
+
namespace: string;
|
|
20
|
+
typeRenames: Record<string, string>;
|
|
21
|
+
ignore: string[];
|
|
22
|
+
// SPDX-style license id, surfaced by the docs-site provenance block. Optional
|
|
23
|
+
// in the config; the docs-site defaults an absent value to "".
|
|
24
|
+
license?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LualsTargets {
|
|
28
|
+
targets: LualsTarget[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const REQUIRED_FIELDS = ["repo", "ref", "sourceGlobs", "moduleId", "namespace"] as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read `luals-targets.json`, validate every required field per entry, and fill
|
|
35
|
+
* optional defaults (`typeRenames` → `{}`, `ignore` → `[]`). Throws on the first
|
|
36
|
+
* missing field naming both the field and the offending entry (its `moduleId`,
|
|
37
|
+
* or its index when `moduleId` itself is absent) — the loud-fail discipline the
|
|
38
|
+
* ts-defold `regenerate` uses for unmapped references. No network.
|
|
39
|
+
*/
|
|
40
|
+
export function readLualsTargets(packageRoot: string): LualsTarget[] {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, "luals-targets.json"), "utf8")) as {
|
|
42
|
+
targets: Partial<LualsTarget>[];
|
|
43
|
+
};
|
|
44
|
+
return parsed.targets.map((entry, index) => {
|
|
45
|
+
const label = typeof entry.moduleId === "string" ? entry.moduleId : `index ${index}`;
|
|
46
|
+
for (const field of REQUIRED_FIELDS) {
|
|
47
|
+
if (entry[field] === undefined) {
|
|
48
|
+
throw new Error(`luals-targets.json: entry ${label} is missing required field "${field}".`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
repo: entry.repo as string,
|
|
53
|
+
ref: entry.ref as string,
|
|
54
|
+
sourceGlobs: entry.sourceGlobs as string[],
|
|
55
|
+
moduleId: entry.moduleId as string,
|
|
56
|
+
namespace: entry.namespace as string,
|
|
57
|
+
typeRenames: entry.typeRenames ?? {},
|
|
58
|
+
ignore: entry.ignore ?? [],
|
|
59
|
+
...(entry.license !== undefined ? { license: entry.license } : {}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compile a glob to an anchored RegExp — mirrors `globToRegex` in
|
|
66
|
+
* `packages/cli/src/build-output.ts` rather than importing across packages
|
|
67
|
+
* (library-types must not depend on the cli package). A `**` path segment spans
|
|
68
|
+
* any number of segments, a bare `**` spans the rest, `*` a non-slash run, `?`
|
|
69
|
+
* one non-slash.
|
|
70
|
+
*/
|
|
71
|
+
function globToRegex(pattern: string): RegExp {
|
|
72
|
+
let out = "";
|
|
73
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
74
|
+
const c = pattern[i];
|
|
75
|
+
if (c === "*") {
|
|
76
|
+
if (pattern[i + 1] === "*") {
|
|
77
|
+
i++;
|
|
78
|
+
if (pattern[i + 1] === "/") {
|
|
79
|
+
i++;
|
|
80
|
+
out += "(?:[^/]+/)*";
|
|
81
|
+
} else {
|
|
82
|
+
out += ".*";
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
out += "[^/]*";
|
|
86
|
+
}
|
|
87
|
+
} else if (c === "?") {
|
|
88
|
+
out += "[^/]";
|
|
89
|
+
} else {
|
|
90
|
+
out += (c as string).replace(/[.+^$(){}|[\]\\]/g, "\\$&");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return new RegExp(`^${out}$`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A path is selected iff at least one `sourceGlob` matches and no `ignore` glob
|
|
98
|
+
* matches. Returns the sorted, deduped subset — the fixture set the vendor step
|
|
99
|
+
* snapshots.
|
|
100
|
+
*/
|
|
101
|
+
export function selectLualsSources(
|
|
102
|
+
paths: string[],
|
|
103
|
+
target: { sourceGlobs: string[]; ignore: string[] },
|
|
104
|
+
): string[] {
|
|
105
|
+
const includes = target.sourceGlobs.map(globToRegex);
|
|
106
|
+
const excludes = target.ignore.map(globToRegex);
|
|
107
|
+
const selected = paths.filter(
|
|
108
|
+
(p) => includes.some((re) => re.test(p)) && !excludes.some((re) => re.test(p)),
|
|
109
|
+
);
|
|
110
|
+
return [...new Set(selected)].sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Enumerate the pinned tree of a LuaLS library repo at its ref. Network seam. */
|
|
114
|
+
export type ListLualsTree = (repo: string, ref: string) => Promise<string[]>;
|
|
115
|
+
|
|
116
|
+
/** Fetch the raw text at a URL. Network seam — mirrors `sync-library-types.ts`. */
|
|
117
|
+
export type FetchText = (url: string) => Promise<string>;
|
|
118
|
+
|
|
119
|
+
/** Recursively enumerate a fixture directory's entries. Filesystem seam. */
|
|
120
|
+
export type ReadFixtureDir = (root: string) => string[];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A GitHub repo URL reduced to the bare `<owner>/<repo>` slug used to address
|
|
124
|
+
* raw content. Mirrors `repoSlug` in the ts-defold front-end.
|
|
125
|
+
*/
|
|
126
|
+
function repoSlug(repo: string): string {
|
|
127
|
+
return repo
|
|
128
|
+
.replace(/^https:\/\/github\.com\//, "")
|
|
129
|
+
.replace(/\.git$/, "")
|
|
130
|
+
.replace(/\/$/, "");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rawUrl(target: LualsTarget, path: string): string {
|
|
134
|
+
return `https://raw.githubusercontent.com/${repoSlug(target.repo)}/${target.ref}/${path}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* List the pinned tree, select the matching sources, fetch each via raw-content
|
|
139
|
+
* URL, and write it under `fixtures/luals/<namespace>/<relpath>` preserving tree
|
|
140
|
+
* shape. Snapshot only — no codemod. The `listTree`/`fetchText` seams keep the
|
|
141
|
+
* pass offline-testable; only the CLI `--fetch` arm wires the real network.
|
|
142
|
+
*/
|
|
143
|
+
export async function fetchLualsFixtures(
|
|
144
|
+
packageRoot: string,
|
|
145
|
+
target: LualsTarget,
|
|
146
|
+
seams: { listTree: ListLualsTree; fetchText: FetchText },
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
const paths = selectLualsSources(await seams.listTree(target.repo, target.ref), target);
|
|
149
|
+
for (const path of paths) {
|
|
150
|
+
const text = await seams.fetchText(rawUrl(target, path));
|
|
151
|
+
const dest = join(packageRoot, "fixtures/luals", target.namespace, path);
|
|
152
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
153
|
+
writeFileSync(dest, text);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse and merge a target's committed fixtures, then build its fidelity report.
|
|
159
|
+
* Reads only `fixtures/luals/<namespace>/**` from disk — zero network — so both
|
|
160
|
+
* the `--fidelity` CLI arm and its round-trip test drive the exact same path and
|
|
161
|
+
* agree byte-for-byte. Fixture files are read in sorted order for determinism,
|
|
162
|
+
* mirroring the parse snapshot.
|
|
163
|
+
*
|
|
164
|
+
* Interfaces and aliases come from the full merge — the module's own signatures
|
|
165
|
+
* reference cross-file classes — but module functions are scoped to the module's
|
|
166
|
+
* own `.lua` file (`moduleId` dotted to a path, e.g. `druid.druid` →
|
|
167
|
+
* `druid/druid.lua`). Merging every file's free functions would export every
|
|
168
|
+
* library file's functions from the one module. A `moduleId` with no matching
|
|
169
|
+
* fixture is a loud misconfiguration, not a silently empty surface.
|
|
170
|
+
*/
|
|
171
|
+
export function buildTargetModel(
|
|
172
|
+
packageRoot: string,
|
|
173
|
+
target: LualsTarget,
|
|
174
|
+
seams: { readDir?: ReadFixtureDir } = {},
|
|
175
|
+
): LibraryModel {
|
|
176
|
+
const readDir = seams.readDir ?? ((r) => readdirSync(r, { recursive: true }).map(String));
|
|
177
|
+
const root = join(packageRoot, "fixtures/luals", target.namespace);
|
|
178
|
+
const files = readDir(root)
|
|
179
|
+
.map((entry) => entry.replace(/\\/g, "/"))
|
|
180
|
+
.filter((entry) => entry.endsWith(".lua"))
|
|
181
|
+
.sort();
|
|
182
|
+
const ownFile = `${target.moduleId.replace(/\./g, "/")}.lua`;
|
|
183
|
+
if (!files.includes(ownFile)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`buildTargetModel: module "${target.moduleId}" expects fixture "${ownFile}" under fixtures/luals/${target.namespace}, but it is not among the ${files.length} fixture .lua files.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const parsed = new Map<string, LibraryModel>();
|
|
189
|
+
for (const rel of files) parsed.set(rel, parseLualsSource(readFileSync(join(root, rel), "utf8")));
|
|
190
|
+
const merged = mergeLibraryModels([...parsed.values()]);
|
|
191
|
+
return { ...merged, moduleFunctions: parsed.get(ownFile)?.moduleFunctions ?? [] };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function buildTargetFidelity(packageRoot: string, target: LualsTarget): FidelityReport {
|
|
195
|
+
return buildFidelityReport(
|
|
196
|
+
target.namespace,
|
|
197
|
+
buildTargetModel(packageRoot, target),
|
|
198
|
+
target.typeRenames,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A druid-style corpus member: a LuaLS-sourced pure-Lua library, distinct from
|
|
204
|
+
* the ts-defold hand-written modules. Standalone registry — the docs-site and
|
|
205
|
+
* CLI wirings belong to later slices, not this one.
|
|
206
|
+
*/
|
|
207
|
+
export interface LualsCorpusEntry {
|
|
208
|
+
moduleId: string;
|
|
209
|
+
namespace: string;
|
|
210
|
+
classification: "pure-lua";
|
|
211
|
+
source: "luals";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function lualsCorpusTargets(packageRoot: string): LualsCorpusEntry[] {
|
|
215
|
+
return readLualsTargets(packageRoot).map((target) => ({
|
|
216
|
+
moduleId: target.moduleId,
|
|
217
|
+
namespace: target.namespace,
|
|
218
|
+
classification: "pure-lua",
|
|
219
|
+
source: "luals",
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface GithubTreeResponse {
|
|
224
|
+
tree?: { path: string }[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const githubHeaders = (): Record<string, string> => {
|
|
228
|
+
const token = process.env.GITHUB_TOKEN;
|
|
229
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const defaultListTree: ListLualsTree = async (repo, ref) => {
|
|
233
|
+
const url = `https://api.github.com/repos/${repoSlug(repo)}/git/trees/${ref}?recursive=1`;
|
|
234
|
+
const res = await fetch(url, { headers: githubHeaders() });
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
throw new Error(`git-trees fetch failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
237
|
+
}
|
|
238
|
+
const body = (await res.json()) as GithubTreeResponse;
|
|
239
|
+
return (body.tree ?? []).map((e) => e.path);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const defaultFetchText: FetchText = async (url) => {
|
|
243
|
+
const res = await fetch(url);
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
throw new Error(`fetch failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
246
|
+
}
|
|
247
|
+
return res.text();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (import.meta.main) {
|
|
251
|
+
const root = join(import.meta.dir, "..");
|
|
252
|
+
const argv = process.argv.slice(2);
|
|
253
|
+
if (argv.includes("--fetch")) {
|
|
254
|
+
const targets = readLualsTargets(root);
|
|
255
|
+
for (const target of targets) {
|
|
256
|
+
await fetchLualsFixtures(root, target, {
|
|
257
|
+
listTree: defaultListTree,
|
|
258
|
+
fetchText: defaultFetchText,
|
|
259
|
+
});
|
|
260
|
+
console.log(`snapshotted ${target.moduleId} from ${repoSlug(target.repo)}@${target.ref}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (argv.includes("--fidelity")) {
|
|
264
|
+
const targets = readLualsTargets(root);
|
|
265
|
+
for (const target of targets) {
|
|
266
|
+
const report = buildTargetFidelity(root, target);
|
|
267
|
+
const dest = join(root, "fidelity", `${target.namespace}.json`);
|
|
268
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
269
|
+
writeFileSync(dest, `${JSON.stringify(report, null, 2)}\n`);
|
|
270
|
+
console.log(
|
|
271
|
+
`${target.moduleId}: coverage ${(report.coverage * 100).toFixed(1)}% (${report.unknownFallbacks} unknown, ${report.undocumentedMembers} undocumented)`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (argv.includes("--emit")) {
|
|
276
|
+
const targets = readLualsTargets(root);
|
|
277
|
+
for (const target of targets) {
|
|
278
|
+
const model = buildTargetModel(root, target);
|
|
279
|
+
const declarations = emitLibraryDeclarations(model, {
|
|
280
|
+
moduleId: target.moduleId,
|
|
281
|
+
typeRenames: target.typeRenames,
|
|
282
|
+
});
|
|
283
|
+
const dest = join(root, "generated", `${target.namespace}.d.ts`);
|
|
284
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
285
|
+
writeFileSync(dest, declarations);
|
|
286
|
+
console.log(`emitted ${target.moduleId} -> generated/${target.namespace}.d.ts`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (argv.includes("--api-doc")) {
|
|
290
|
+
const targets = readLualsTargets(root);
|
|
291
|
+
for (const target of targets) {
|
|
292
|
+
const model = buildTargetModel(root, target);
|
|
293
|
+
const lowered = lowerLibraryModel(model, {
|
|
294
|
+
namespace: target.namespace,
|
|
295
|
+
typeRenames: target.typeRenames,
|
|
296
|
+
});
|
|
297
|
+
const dest = join(root, "api-doc", `${target.namespace}.json`);
|
|
298
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
299
|
+
writeFileSync(dest, `${JSON.stringify(lowered, null, 2)}\n`);
|
|
300
|
+
console.log(`lowered ${target.moduleId} -> api-doc/${target.namespace}.json`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|