@defold-typescript/library-types 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +52 -0
- package/README.md +78 -0
- package/api-doc/boom.boom.json +26 -0
- package/api-doc/bridge.bridge.json +1653 -0
- package/api-doc/bzAnim.bzLibrary.json +156 -0
- package/api-doc/defcon.console.json +109 -0
- package/api-doc/defmath.defmath.json +1381 -0
- package/api-doc/defsave.defsave.json +175 -0
- package/api-doc/deftest.deftest.json +71 -0
- package/api-doc/dicebag.dicebag.json +323 -0
- package/api-doc/event.event.json +238 -0
- package/api-doc/gooey.gooey.json +963 -0
- package/api-doc/immutable.immutable.json +63 -0
- package/api-doc/in.accelerometer.json +281 -0
- package/api-doc/in.button.json +148 -0
- package/api-doc/in.cursor.json +203 -0
- package/api-doc/in.gesture.json +206 -0
- package/api-doc/in.keyboard.json +45 -0
- package/api-doc/in.mapper.json +141 -0
- package/api-doc/in.onscreen.json +265 -0
- package/api-doc/in.state.json +195 -0
- package/api-doc/in.textbox.json +213 -0
- package/api-doc/in.triggers.json +1260 -0
- package/api-doc/lang.lang.json +411 -0
- package/api-doc/log.log.json +50 -0
- package/api-doc/metrics.fps.json +88 -0
- package/api-doc/metrics.mem.json +80 -0
- package/api-doc/monarch.monarch.json +1065 -0
- package/api-doc/monarch.transitions.easings.json +206 -0
- package/api-doc/monarch.transitions.gui.json +445 -0
- package/api-doc/nakama.engine.defold.json +165 -0
- package/api-doc/nakama.nakama.json +6574 -0
- package/api-doc/nakama.util.log.json +25 -0
- package/api-doc/narrator.narrator.json +150 -0
- package/api-doc/orthographic.camera.json +1019 -0
- package/api-doc/persist.persist.json +142 -0
- package/api-doc/platypus.platypus.json +631 -0
- package/api-doc/proto.proto.json +355 -0
- package/api-doc/rendy.rendy.json +408 -0
- package/api-doc/richtext.color.json +219 -0
- package/api-doc/richtext.richtext.json +382 -0
- package/api-doc/richtext.tags.json +67 -0
- package/api-doc/saver.saver.json +553 -0
- package/api-doc/saver.storage.json +174 -0
- package/api-doc/squid.squid.json +660 -0
- package/api-doc/starly.starly.json +488 -0
- package/api-doc/tweener.tweener.json +419 -0
- package/api-doc/yagames.yagames.json +1465 -0
- package/api-doc/zzfx.api.json +85 -0
- package/generated/boom.boom.d.ts +1585 -0
- package/generated/bridge.bridge.d.ts +533 -0
- package/generated/bzAnim.bzLibrary.d.ts +93 -0
- package/generated/defcon.console.d.ts +24 -0
- package/generated/defmath.defmath.d.ts +194 -0
- package/generated/defsave.defsave.d.ts +31 -0
- package/generated/deftest.deftest.d.ts +47 -0
- package/generated/dicebag.dicebag.d.ts +90 -0
- package/generated/event.event.d.ts +54 -0
- package/generated/gooey.gooey.d.ts +261 -0
- package/generated/immutable.immutable.d.ts +13 -0
- package/generated/in.accelerometer.d.ts +37 -0
- package/generated/in.button.d.ts +20 -0
- package/generated/in.cursor.d.ts +33 -0
- package/generated/in.gesture.d.ts +64 -0
- package/generated/in.keyboard.d.ts +18 -0
- package/generated/in.mapper.d.ts +23 -0
- package/generated/in.onscreen.d.ts +58 -0
- package/generated/in.state.d.ts +34 -0
- package/generated/in.textbox.d.ts +26 -0
- package/generated/in.triggers.d.ts +180 -0
- package/generated/lang.lang.d.ts +33 -0
- package/generated/log.log.d.ts +40 -0
- package/generated/metrics.fps.d.ts +16 -0
- package/generated/metrics.mem.d.ts +16 -0
- package/generated/monarch.monarch.d.ts +412 -0
- package/generated/monarch.transitions.easings.d.ts +65 -0
- package/generated/monarch.transitions.gui.d.ts +197 -0
- package/generated/nakama.engine.defold.d.ts +24 -0
- package/generated/nakama.nakama.d.ts +594 -0
- package/generated/nakama.util.log.d.ts +10 -0
- package/generated/narrator.narrator.d.ts +66 -0
- package/generated/orthographic.camera.d.ts +308 -0
- package/generated/persist.persist.d.ts +34 -0
- package/generated/platypus.platypus.d.ts +76 -0
- package/generated/proto.proto.d.ts +36 -0
- package/generated/rendy.rendy.d.ts +184 -0
- package/generated/richtext.color.d.ts +33 -0
- package/generated/richtext.richtext.d.ts +126 -0
- package/generated/richtext.tags.d.ts +12 -0
- package/generated/saver.saver.d.ts +44 -0
- package/generated/saver.storage.d.ts +16 -0
- package/generated/squid.squid.d.ts +106 -0
- package/generated/starly.starly.d.ts +148 -0
- package/generated/tweener.tweener.d.ts +151 -0
- package/generated/yagames.yagames.d.ts +279 -0
- package/generated/zzfx.api.d.ts +20 -0
- package/library-classification.json +726 -0
- package/library-targets.json +291 -0
- package/package.json +180 -0
- package/scripts/extract-api-doc.ts +418 -0
- package/scripts/sync-library-types.ts +542 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { extractApiDoc } from "./extract-api-doc";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ts-defold/library core-type references -> the @defold-typescript/types surface.
|
|
8
|
+
* Dotted `vmath.*` names collapse to a single core-type name; bare handle tokens
|
|
9
|
+
* become `Opaque<…>` brands, resolving against the globals in
|
|
10
|
+
* `@defold-typescript/types` `engine-globals.d.ts`. `table` is intentionally
|
|
11
|
+
* absent: ts-defold modules declare their own local `type table = {}` alias, so
|
|
12
|
+
* renaming it would rewrite an unrelated local type.
|
|
13
|
+
*/
|
|
14
|
+
export const CORE_TYPE_RENAMES: Readonly<Record<string, string>> = {
|
|
15
|
+
"vmath.vector": "Vector",
|
|
16
|
+
"vmath.vector3": "Vector3",
|
|
17
|
+
"vmath.vector4": "Vector4",
|
|
18
|
+
"vmath.matrix4": "Matrix4",
|
|
19
|
+
"vmath.quat": "Quaternion",
|
|
20
|
+
"vmath.quaternion": "Quaternion",
|
|
21
|
+
hash: "Hash",
|
|
22
|
+
url: "Url",
|
|
23
|
+
node: 'Opaque<"node">',
|
|
24
|
+
texture: 'Opaque<"texture">',
|
|
25
|
+
render_target: 'Opaque<"render_target">',
|
|
26
|
+
constant: 'Opaque<"constant">',
|
|
27
|
+
constant_buffer: 'Opaque<"constant_buffer">',
|
|
28
|
+
buffer: 'Opaque<"buffer">',
|
|
29
|
+
bufferstream: 'Opaque<"bufferstream">',
|
|
30
|
+
resource: 'Opaque<"resource">',
|
|
31
|
+
userdata: 'Opaque<"userdata">',
|
|
32
|
+
b2World: 'Opaque<"b2World">',
|
|
33
|
+
b2Body: 'Opaque<"b2Body">',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface CodemodResult {
|
|
37
|
+
output: string;
|
|
38
|
+
unmapped: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function entityNameText(name: ts.EntityName): string {
|
|
42
|
+
return ts.isIdentifier(name) ? name.text : `${entityNameText(name.left)}.${name.right.text}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rename every Defold core-type reference in an ambient `.d.ts` to the
|
|
47
|
+
* @defold-typescript/types surface. Matches type references only — property
|
|
48
|
+
* names, JSDoc, `declare module`, and passthrough extensions (`LuaMultiReturn`,
|
|
49
|
+
* `LuaMap`) stay byte-identical because `forEachChild` never descends into
|
|
50
|
+
* identifiers-as-names or comments. Any `vmath.*` reference with no mapping is
|
|
51
|
+
* collected into `unmapped` and left verbatim so a missing rename surfaces as a
|
|
52
|
+
* red typecheck rather than a silent `any`.
|
|
53
|
+
*/
|
|
54
|
+
export function codemodDeclaration(source: string): CodemodResult {
|
|
55
|
+
const sf = ts.createSourceFile(
|
|
56
|
+
"module.d.ts",
|
|
57
|
+
source,
|
|
58
|
+
ts.ScriptTarget.Latest,
|
|
59
|
+
/* setParentNodes */ true,
|
|
60
|
+
ts.ScriptKind.TS,
|
|
61
|
+
);
|
|
62
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
63
|
+
const unmapped = new Set<string>();
|
|
64
|
+
|
|
65
|
+
const visit = (node: ts.Node): void => {
|
|
66
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
67
|
+
const name = entityNameText(node.typeName);
|
|
68
|
+
const rename = CORE_TYPE_RENAMES[name];
|
|
69
|
+
if (rename !== undefined) {
|
|
70
|
+
edits.push({
|
|
71
|
+
start: node.typeName.getStart(sf),
|
|
72
|
+
end: node.typeName.getEnd(),
|
|
73
|
+
text: rename,
|
|
74
|
+
});
|
|
75
|
+
} else if (name.startsWith("vmath.")) {
|
|
76
|
+
unmapped.add(name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
ts.forEachChild(node, visit);
|
|
80
|
+
};
|
|
81
|
+
visit(sf);
|
|
82
|
+
|
|
83
|
+
edits.sort((a, b) => b.start - a.start);
|
|
84
|
+
let output = source;
|
|
85
|
+
for (const edit of edits) {
|
|
86
|
+
output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
87
|
+
}
|
|
88
|
+
return { output, unmapped: [...unmapped] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface LibrarySource {
|
|
92
|
+
repo: string;
|
|
93
|
+
commit: string;
|
|
94
|
+
license: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface LibraryTarget {
|
|
98
|
+
module: string;
|
|
99
|
+
path: string;
|
|
100
|
+
fixture: string;
|
|
101
|
+
generated: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface LibraryTargets {
|
|
105
|
+
source: LibrarySource;
|
|
106
|
+
targets: LibraryTarget[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readTargets(packageRoot: string): LibraryTargets {
|
|
110
|
+
return JSON.parse(
|
|
111
|
+
readFileSync(join(packageRoot, "library-targets.json"), "utf8"),
|
|
112
|
+
) as LibraryTargets;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read every pinned fixture, codemod it, and write the renamed `declare module`
|
|
117
|
+
* to its `generated/` path. Throws on the first unmapped reference so a stale
|
|
118
|
+
* rename table never ships a silently-broken type.
|
|
119
|
+
*/
|
|
120
|
+
export function regenerate(packageRoot: string): void {
|
|
121
|
+
const targets = readTargets(packageRoot);
|
|
122
|
+
for (const target of targets.targets) {
|
|
123
|
+
const source = readFileSync(join(packageRoot, target.fixture), "utf8");
|
|
124
|
+
const { output, unmapped } = codemodDeclaration(source);
|
|
125
|
+
if (unmapped.length > 0) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`${target.module}: unmapped core-type references ${unmapped.join(", ")} — extend CORE_TYPE_RENAMES.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
writeFileSync(join(packageRoot, target.generated), output);
|
|
131
|
+
}
|
|
132
|
+
writeApiDocs(packageRoot);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The pretty-printed ref-doc JSON a generated module extracts to. */
|
|
136
|
+
function apiDocJson(packageRoot: string, target: LibraryTarget): string {
|
|
137
|
+
const generated = readFileSync(join(packageRoot, target.generated), "utf8");
|
|
138
|
+
return `${JSON.stringify(extractApiDoc(generated, target.module), null, 2)}\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract every generated `declare module` into a committed
|
|
143
|
+
* `api-doc/<module>.json` ref-doc fixture the docs-site loads via
|
|
144
|
+
* `parseDefoldApiDoc`. Runs at the tail of `regenerate` so the fixtures never
|
|
145
|
+
* drift from the `.d.ts` they describe.
|
|
146
|
+
*/
|
|
147
|
+
export function writeApiDocs(packageRoot: string): void {
|
|
148
|
+
const { targets } = readTargets(packageRoot);
|
|
149
|
+
mkdirSync(join(packageRoot, "api-doc"), { recursive: true });
|
|
150
|
+
for (const target of targets) {
|
|
151
|
+
writeFileSync(
|
|
152
|
+
join(packageRoot, "api-doc", `${target.module}.json`),
|
|
153
|
+
apiDocJson(packageRoot, target),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Verify each committed `api-doc/<module>.json` still equals a fresh extraction
|
|
160
|
+
* from its generated file, without rewriting — the `--check` counterpart to
|
|
161
|
+
* `writeApiDocs`. A missing or stale fixture reports `false`.
|
|
162
|
+
*/
|
|
163
|
+
export function checkApiDocs(packageRoot: string): { module: string; ok: boolean }[] {
|
|
164
|
+
const { targets } = readTargets(packageRoot);
|
|
165
|
+
return targets.map((target) => {
|
|
166
|
+
const expected = apiDocJson(packageRoot, target);
|
|
167
|
+
let committed = "";
|
|
168
|
+
try {
|
|
169
|
+
committed = readFileSync(join(packageRoot, "api-doc", `${target.module}.json`), "utf8");
|
|
170
|
+
} catch {
|
|
171
|
+
committed = "";
|
|
172
|
+
}
|
|
173
|
+
return { module: target.module, ok: committed === expected };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verify the curated overrides are applied in the committed
|
|
179
|
+
* `library-descriptions.json` without doing a network fetch. Fetched-only
|
|
180
|
+
* descriptions use the committed file as the baseline and round-trip unchanged;
|
|
181
|
+
* stale override entries surface so `bun run sync --check` exits red.
|
|
182
|
+
*/
|
|
183
|
+
export function checkDescriptions(packageRoot: string): string[] {
|
|
184
|
+
let committed: Record<string, string> = {};
|
|
185
|
+
const path = join(packageRoot, "library-descriptions.json");
|
|
186
|
+
if (existsSync(path)) {
|
|
187
|
+
committed = JSON.parse(readFileSync(path, "utf8")) as Record<string, string>;
|
|
188
|
+
}
|
|
189
|
+
const expected = mergeLibraryDescriptions(committed, readOverrides(packageRoot));
|
|
190
|
+
const allDirs = new Set<string>([...Object.keys(expected), ...Object.keys(committed)]);
|
|
191
|
+
return [...allDirs].filter((dir) => expected[dir] !== committed[dir]).sort();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A GitHub repo URL (`https://github.com/<owner>/<repo>[.git]`) reduced to the
|
|
196
|
+
* bare `<owner>/<repo>` slug used to address raw content and the git-trees API.
|
|
197
|
+
* A non-github URL is returned unchanged so callers can decide whether to skip
|
|
198
|
+
* it (the descriptions pass does; the raw-content / tree passes don't).
|
|
199
|
+
*/
|
|
200
|
+
export function repoSlug(repo: string): string {
|
|
201
|
+
return repo
|
|
202
|
+
.replace(/^https:\/\/github\.com\//, "")
|
|
203
|
+
.replace(/\.git$/, "")
|
|
204
|
+
.replace(/\/$/, "");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const GITHUB_REPO_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?\/?$/;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The raw.githubusercontent.com URL for a target's upstream `.d.ts` at the
|
|
211
|
+
* pinned commit. `source.repo` is the human `https://github.com/<owner>/<repo>`
|
|
212
|
+
* form; raw content is addressed by the bare `<owner>/<repo>` slug.
|
|
213
|
+
*/
|
|
214
|
+
export function rawUrl(source: LibrarySource, target: LibraryTarget): string {
|
|
215
|
+
return `https://raw.githubusercontent.com/${repoSlug(source.repo)}/${source.commit}/${target.path}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type DriftStatus = "ok" | "upstream-drift" | "transform-drift";
|
|
219
|
+
|
|
220
|
+
export interface DriftResult {
|
|
221
|
+
module: string;
|
|
222
|
+
status: DriftStatus;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type FetchText = (url: string) => Promise<string>;
|
|
226
|
+
|
|
227
|
+
const defaultFetch: FetchText = async (url) => {
|
|
228
|
+
const res = await fetch(url);
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
throw new Error(`fetch failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
231
|
+
}
|
|
232
|
+
return res.text();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Live-fetch each pinned upstream `.d.ts` and classify it against the committed
|
|
237
|
+
* files: `upstream-drift` when the fetched bytes no longer match the committed
|
|
238
|
+
* fixture (upstream moved off the pin), `transform-drift` when the fixture is
|
|
239
|
+
* unchanged but the committed generated file differs from a fresh codemod (the
|
|
240
|
+
* codemod or fixture changed without a `bun regen`). The `fetchText` seam keeps
|
|
241
|
+
* the classifier offline-testable; only the CLI wires the real network.
|
|
242
|
+
*/
|
|
243
|
+
export async function checkDrift(
|
|
244
|
+
packageRoot: string,
|
|
245
|
+
fetchText: FetchText = defaultFetch,
|
|
246
|
+
): Promise<DriftResult[]> {
|
|
247
|
+
const { source, targets } = readTargets(packageRoot);
|
|
248
|
+
const results: DriftResult[] = [];
|
|
249
|
+
for (const target of targets) {
|
|
250
|
+
const fetched = await fetchText(rawUrl(source, target));
|
|
251
|
+
const fixture = readFileSync(join(packageRoot, target.fixture), "utf8");
|
|
252
|
+
let status: DriftStatus;
|
|
253
|
+
if (fetched !== fixture) {
|
|
254
|
+
status = "upstream-drift";
|
|
255
|
+
} else {
|
|
256
|
+
const generated = readFileSync(join(packageRoot, target.generated), "utf8");
|
|
257
|
+
status = codemodDeclaration(fetched).output === generated ? "ok" : "transform-drift";
|
|
258
|
+
}
|
|
259
|
+
results.push({ module: target.module, status });
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type DirClassification = "pure-lua" | "native" | "already-vendored" | "covered-by-goal";
|
|
265
|
+
|
|
266
|
+
export interface ClassificationEntry {
|
|
267
|
+
dir: string;
|
|
268
|
+
classification: DirClassification;
|
|
269
|
+
modules: string[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A ts-defold/library `<name>-<version>.d.ts` alias file (e.g. `monarch-5.1.0`,
|
|
273
|
+
// `taptic_engine-1.2`) — the "latest" pointer, not a distinct module name.
|
|
274
|
+
const VERSION_ALIAS = /-\d+\.\d+(\.\d+)?$/;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Group the vendored module names in a ts-defold/library tree by library dir.
|
|
278
|
+
* Every library lives at `packages/<dir>/<module>.d.ts`; each contributes
|
|
279
|
+
* `<module>`, minus the versioned alias files (`<name>-<semver>.d.ts`). A dir
|
|
280
|
+
* is registered from any path under it, so a dir carrying only a `library.json`
|
|
281
|
+
* (or nothing but an alias) maps to `[]`. Root files, dot-prefixed dirs, and
|
|
282
|
+
* non-`packages/` paths are ignored. Module lists are sorted for stable output.
|
|
283
|
+
*/
|
|
284
|
+
export function libraryModulesFromTree(paths: string[]): Map<string, string[]> {
|
|
285
|
+
const byDir = new Map<string, string[]>();
|
|
286
|
+
for (const p of paths) {
|
|
287
|
+
const segments = p.split("/");
|
|
288
|
+
if (segments[0] !== "packages" || segments.length < 3) continue;
|
|
289
|
+
const dir = segments[1];
|
|
290
|
+
if (dir === undefined || dir.startsWith(".")) continue;
|
|
291
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
292
|
+
const file = segments[segments.length - 1];
|
|
293
|
+
if (file === undefined || !file.endsWith(".d.ts")) continue;
|
|
294
|
+
const moduleName = file.slice(0, -".d.ts".length);
|
|
295
|
+
if (VERSION_ALIAS.test(moduleName)) continue;
|
|
296
|
+
byDir.get(dir)?.push(moduleName);
|
|
297
|
+
}
|
|
298
|
+
for (const modules of byDir.values()) modules.sort();
|
|
299
|
+
return byDir;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Classify each library dir from its module-name shape. A Defold native
|
|
304
|
+
* extension registers a bare global module (`daabbcc`, `astar`), while a
|
|
305
|
+
* pure-Lua library is required by a dotted path (`monarch.monarch`,
|
|
306
|
+
* `richtext.richtext`) — so a dir is `pure-lua` iff it has at least one module
|
|
307
|
+
* and every module name is dotted; any bare module (or none at all) means
|
|
308
|
+
* `native`. The exclusion sets win: a vendored or goal-covered dir keeps that
|
|
309
|
+
* label regardless of shape. The module names are recorded as the classification
|
|
310
|
+
* evidence. Sorted by `dir` for a stable committed manifest.
|
|
311
|
+
*/
|
|
312
|
+
export function classifyLibraryDirs(
|
|
313
|
+
dirs: { dir: string; modules: string[] }[],
|
|
314
|
+
opts: { vendoredDirs: ReadonlySet<string>; coveredByGoalDirs: ReadonlySet<string> },
|
|
315
|
+
): ClassificationEntry[] {
|
|
316
|
+
return dirs
|
|
317
|
+
.map(({ dir, modules }): ClassificationEntry => {
|
|
318
|
+
let classification: DirClassification;
|
|
319
|
+
if (opts.vendoredDirs.has(dir)) {
|
|
320
|
+
classification = "already-vendored";
|
|
321
|
+
} else if (opts.coveredByGoalDirs.has(dir)) {
|
|
322
|
+
classification = "covered-by-goal";
|
|
323
|
+
} else if (modules.length > 0 && modules.every((m) => m.includes("."))) {
|
|
324
|
+
classification = "pure-lua";
|
|
325
|
+
} else {
|
|
326
|
+
classification = "native";
|
|
327
|
+
}
|
|
328
|
+
return { dir, classification, modules };
|
|
329
|
+
})
|
|
330
|
+
.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface GithubTreeResponse {
|
|
334
|
+
tree?: { path: string }[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface GithubRepoResponse {
|
|
338
|
+
description?: string | null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const githubHeaders = (): Record<string, string> => {
|
|
342
|
+
const token = process.env.GITHUB_TOKEN;
|
|
343
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
async function githubTreePaths(slug: string, ref: string): Promise<string[]> {
|
|
347
|
+
const url = `https://api.github.com/repos/${slug}/git/trees/${ref}?recursive=1`;
|
|
348
|
+
const res = await fetch(url, { headers: githubHeaders() });
|
|
349
|
+
if (!res.ok) {
|
|
350
|
+
throw new Error(`git-trees fetch failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
351
|
+
}
|
|
352
|
+
const body = (await res.json()) as GithubTreeResponse;
|
|
353
|
+
return (body.tree ?? []).map((e) => e.path);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Fetch the GitHub `description` field for `<owner>/<repo>` via the repos API.
|
|
358
|
+
* Returns the trimmed string, or `""` when the field is missing/null/blank.
|
|
359
|
+
* Network seam kept out of CI by the `--descriptions` CLI arm.
|
|
360
|
+
*/
|
|
361
|
+
export type FetchRepoDescription = (owner: string, repo: string) => Promise<string>;
|
|
362
|
+
|
|
363
|
+
const defaultFetchRepoDescription: FetchRepoDescription = async (owner, repo) => {
|
|
364
|
+
const url = `https://api.github.com/repos/${owner}/${repo}`;
|
|
365
|
+
const res = await fetch(url, { headers: githubHeaders() });
|
|
366
|
+
if (!res.ok) {
|
|
367
|
+
throw new Error(`repo fetch failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
368
|
+
}
|
|
369
|
+
const body = (await res.json()) as GithubRepoResponse;
|
|
370
|
+
return (body.description ?? "").trim();
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/** Enumerate the ts-defold/library tree at the pinned commit. Network seam. */
|
|
374
|
+
export type ListTree = (source: LibrarySource) => Promise<string[]>;
|
|
375
|
+
|
|
376
|
+
const defaultListTree: ListTree = (source) => githubTreePaths(repoSlug(source.repo), source.commit);
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Enumerate every ts-defold/library dir at the pin, classify each by its
|
|
380
|
+
* module-name shape, and write `library-classification.json`. The `listTree`
|
|
381
|
+
* seam keeps the pass offline-testable; only the CLI wires the real call, and it
|
|
382
|
+
* stays out of CI (mirrors `--check`). The manifest pins the same `source` as
|
|
383
|
+
* `library-targets.json`.
|
|
384
|
+
*/
|
|
385
|
+
export async function writeClassification(
|
|
386
|
+
packageRoot: string,
|
|
387
|
+
seams: { listTree?: ListTree } = {},
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
const listTree = seams.listTree ?? defaultListTree;
|
|
390
|
+
const { source, targets } = readTargets(packageRoot);
|
|
391
|
+
const vendoredDirs = new Set(
|
|
392
|
+
targets.map((t) => t.path.split("/")[1]).filter((d): d is string => d !== undefined),
|
|
393
|
+
);
|
|
394
|
+
const coveredByGoalDirs = new Set(["defold-lldebugger", "defold-xmath"]);
|
|
395
|
+
|
|
396
|
+
const modulesByDir = libraryModulesFromTree(await listTree(source));
|
|
397
|
+
const dirs = [...modulesByDir].map(([dir, modules]) => ({ dir, modules }));
|
|
398
|
+
const entries = classifyLibraryDirs(dirs, { vendoredDirs, coveredByGoalDirs });
|
|
399
|
+
writeFileSync(
|
|
400
|
+
join(packageRoot, "library-classification.json"),
|
|
401
|
+
`${JSON.stringify({ source, dirs: entries }, null, 2)}\n`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// `library-description-overrides.json` shape: `{ "<dir>": "<one-line description>" }`.
|
|
406
|
+
// Curated entries win over the fetched GitHub description, so empty or poorly-
|
|
407
|
+
// phrased upstream descriptions can be patched without a network run. The
|
|
408
|
+
// committed map seeds every dir that has no `info.description` in its api-doc
|
|
409
|
+
// fixture, so a fresh checkout ships every library page with a non-empty intro.
|
|
410
|
+
export type DescriptionOverrides = Readonly<Record<string, string>>;
|
|
411
|
+
|
|
412
|
+
function readOverrides(packageRoot: string): DescriptionOverrides {
|
|
413
|
+
const path = join(packageRoot, "library-description-overrides.json");
|
|
414
|
+
if (!existsSync(path)) return {};
|
|
415
|
+
return JSON.parse(readFileSync(path, "utf8")) as DescriptionOverrides;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Pure merge of `fetched` descriptions with the curated `overrides` map.
|
|
420
|
+
* Override wins per-dir; a missing or blank value (in either source) means the
|
|
421
|
+
* dir is dropped; output is sorted by key for stable committed output.
|
|
422
|
+
*/
|
|
423
|
+
export function mergeLibraryDescriptions(
|
|
424
|
+
fetched: Readonly<Record<string, string>>,
|
|
425
|
+
overrides: Readonly<Record<string, string>>,
|
|
426
|
+
): Record<string, string> {
|
|
427
|
+
const dirs = new Set<string>([...Object.keys(fetched), ...Object.keys(overrides)]);
|
|
428
|
+
const merged: Record<string, string> = {};
|
|
429
|
+
for (const dir of dirs) {
|
|
430
|
+
const text = (overrides[dir] ?? fetched[dir] ?? "").trim();
|
|
431
|
+
if (text.length > 0) merged[dir] = text;
|
|
432
|
+
}
|
|
433
|
+
return Object.fromEntries(
|
|
434
|
+
Object.entries(merged).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A NOTICE credit line: `- <dir> — <author>, <url>`.
|
|
439
|
+
const NOTICE_CREDIT = /^\s*-\s+(\S+)\s+—\s+(.+?),\s+(https?:\/\/\S+)\s*$/;
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Read the `NOTICE` credit table and resolve each dir's upstream `owner/repo`
|
|
443
|
+
* from its GitHub URL. A non-github URL or an entry with no URL is skipped — the
|
|
444
|
+
* vendored library still gets a description from `overrides`, but no network
|
|
445
|
+
* fetch is attempted for it.
|
|
446
|
+
*/
|
|
447
|
+
function parseNoticeRepoSlugs(packageRoot: string): Map<string, { owner: string; repo: string }> {
|
|
448
|
+
const path = join(packageRoot, "NOTICE");
|
|
449
|
+
if (!existsSync(path)) return new Map();
|
|
450
|
+
const slugs = new Map<string, { owner: string; repo: string }>();
|
|
451
|
+
for (const raw of readFileSync(path, "utf8").split("\n")) {
|
|
452
|
+
const match = NOTICE_CREDIT.exec(raw);
|
|
453
|
+
if (!match) continue;
|
|
454
|
+
const dir = match[1];
|
|
455
|
+
const url = match[3];
|
|
456
|
+
if (!dir || !url) continue;
|
|
457
|
+
const gh = GITHUB_REPO_URL.exec(url);
|
|
458
|
+
if (!gh) continue;
|
|
459
|
+
const owner = gh[1];
|
|
460
|
+
const repo = gh[2];
|
|
461
|
+
if (!owner || !repo) continue;
|
|
462
|
+
slugs.set(dir, { owner, repo });
|
|
463
|
+
}
|
|
464
|
+
return slugs;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Fetch each NOTICE-credited upstream's GitHub `description`, apply the curated
|
|
469
|
+
* overrides, and write `library-descriptions.json` as a sorted `{ dir: text }`
|
|
470
|
+
* map. Dirs with no upstream GitHub URL contribute only via overrides; dirs
|
|
471
|
+
* that fetch to empty AND have no override are dropped.
|
|
472
|
+
*/
|
|
473
|
+
export async function writeDescriptions(
|
|
474
|
+
packageRoot: string,
|
|
475
|
+
seams: {
|
|
476
|
+
fetchRepoDescription?: FetchRepoDescription;
|
|
477
|
+
overrides?: DescriptionOverrides;
|
|
478
|
+
} = {},
|
|
479
|
+
): Promise<void> {
|
|
480
|
+
const fetchRepoDescription = seams.fetchRepoDescription ?? defaultFetchRepoDescription;
|
|
481
|
+
const overrides = seams.overrides ?? readOverrides(packageRoot);
|
|
482
|
+
|
|
483
|
+
const slugs = parseNoticeRepoSlugs(packageRoot);
|
|
484
|
+
const fetched: Record<string, string> = {};
|
|
485
|
+
for (const [dir, { owner, repo }] of slugs) {
|
|
486
|
+
fetched[dir] = await fetchRepoDescription(owner, repo);
|
|
487
|
+
}
|
|
488
|
+
writeFileSync(
|
|
489
|
+
join(packageRoot, "library-descriptions.json"),
|
|
490
|
+
`${JSON.stringify(mergeLibraryDescriptions(fetched, overrides), null, 2)}\n`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (import.meta.main) {
|
|
495
|
+
const root = join(import.meta.dir, "..");
|
|
496
|
+
const argv = process.argv.slice(2);
|
|
497
|
+
if (argv.includes("--classify")) {
|
|
498
|
+
await writeClassification(root);
|
|
499
|
+
const { dirs } = JSON.parse(
|
|
500
|
+
readFileSync(join(root, "library-classification.json"), "utf8"),
|
|
501
|
+
) as { dirs: ClassificationEntry[] };
|
|
502
|
+
const counts = new Map<DirClassification, number>();
|
|
503
|
+
for (const e of dirs) counts.set(e.classification, (counts.get(e.classification) ?? 0) + 1);
|
|
504
|
+
console.log(`classified ${dirs.length} upstream dir(s) from ts-defold/library`);
|
|
505
|
+
for (const [classification, n] of [...counts].sort()) {
|
|
506
|
+
console.log(` ${classification}: ${n}`);
|
|
507
|
+
}
|
|
508
|
+
} else if (argv.includes("--descriptions")) {
|
|
509
|
+
await writeDescriptions(root);
|
|
510
|
+
const descriptions = JSON.parse(
|
|
511
|
+
readFileSync(join(root, "library-descriptions.json"), "utf8"),
|
|
512
|
+
) as Record<string, string>;
|
|
513
|
+
console.log(
|
|
514
|
+
`wrote ${Object.keys(descriptions).length} description(s) to library-descriptions.json`,
|
|
515
|
+
);
|
|
516
|
+
} else if (argv.includes("--check")) {
|
|
517
|
+
const results = await checkDrift(root);
|
|
518
|
+
console.log(`checked ${results.length} vendored target(s) against ts-defold/library`);
|
|
519
|
+
for (const { module, status } of results) {
|
|
520
|
+
console.log(` ${status}: ${module}`);
|
|
521
|
+
}
|
|
522
|
+
const apiDocs = checkApiDocs(root);
|
|
523
|
+
console.log(`checked ${apiDocs.length} api-doc fixture(s)`);
|
|
524
|
+
for (const { module, ok } of apiDocs) {
|
|
525
|
+
if (!ok) console.log(` api-doc-drift: ${module}`);
|
|
526
|
+
}
|
|
527
|
+
const descriptionDrift = checkDescriptions(root);
|
|
528
|
+
if (descriptionDrift.length > 0) {
|
|
529
|
+
console.log(`checked library-descriptions.json`);
|
|
530
|
+
for (const dir of descriptionDrift) console.log(` description-drift: ${dir}`);
|
|
531
|
+
}
|
|
532
|
+
if (
|
|
533
|
+
results.some((r) => r.status !== "ok") ||
|
|
534
|
+
apiDocs.some((r) => !r.ok) ||
|
|
535
|
+
descriptionDrift.length > 0
|
|
536
|
+
) {
|
|
537
|
+
process.exitCode = 1;
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
regenerate(root);
|
|
541
|
+
}
|
|
542
|
+
}
|