@defold-typescript/library-types 0.23.0 → 0.25.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/NOTICE +2 -3
- package/api-doc/boom.json +3872 -0
- package/api-doc/bridge.json +10 -5
- package/api-doc/bzAnim.json +338 -0
- package/api-doc/checkpoint.json +127 -0
- package/api-doc/decore.json +1 -1
- 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 +745 -0
- package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
- package/api-doc/druid.json +90 -92
- package/api-doc/event.json +16 -16
- package/api-doc/gooey.json +2050 -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 +5 -5
- package/api-doc/log.json +1 -1
- 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.session.json +315 -0
- package/api-doc/nakama.socket.json +3356 -0
- package/api-doc/nakama.util.log.json +32 -4
- package/api-doc/narrator.json +1 -1
- 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/{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 +2 -1
- package/api-doc/saver.storage.json +2 -1
- package/api-doc/shutter.json +551 -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 +433 -0
- package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
- package/generated/bridge.d.ts +5 -5
- package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
- package/generated/checkpoint.d.ts +62 -0
- package/generated/decore.d.ts +1 -1
- 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 +81 -79
- package/generated/event.d.ts +15 -15
- package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
- package/generated/in.onscreen.d.ts +5 -0
- package/generated/in.textbox.d.ts +4 -0
- package/generated/lang.d.ts +5 -5
- package/generated/log.d.ts +1 -1
- 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.session.d.ts +95 -0
- package/generated/nakama.socket.d.ts +706 -0
- package/generated/nakama.util.log.d.ts +6 -0
- package/generated/narrator.d.ts +1 -1
- 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 +4 -4
- 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 +2 -0
- package/generated/saver.storage.d.ts +2 -0
- package/generated/shutter.d.ts +183 -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 -156
- package/library-targets.json +1 -218
- package/luals-targets.json +56 -3
- package/openapi-targets.json +17 -0
- package/package.json +7 -57
- package/scripts/__snapshots__/parse-luals.test.ts.snap +164 -10
- package/scripts/apply-luals-overrides.ts +39 -7
- package/scripts/authored-parity.ts +718 -0
- package/scripts/emit-library-dts.ts +94 -17
- package/scripts/extract-api-doc.ts +214 -42
- package/scripts/fidelity-floor.ts +178 -0
- package/scripts/import-upstream-docs.ts +125 -0
- package/scripts/lower-api-doc.ts +21 -3
- package/scripts/luals-fidelity.ts +16 -11
- package/scripts/map-luals-types.ts +52 -21
- package/scripts/parse-lua-surface.ts +187 -0
- package/scripts/parse-luals.ts +50 -4
- 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 +14 -1
- package/scripts/sync-markdown-types.ts +761 -0
- package/scripts/sync-openapi-types.ts +308 -0
- package/scripts/sync-script-api-types.ts +16 -17
- 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/starly.starly.json +0 -488
- package/generated/defcon.console.d.ts +0 -24
- package/generated/defsave.defsave.d.ts +0 -31
- package/generated/nakama.nakama.d.ts +0 -594
- package/generated/starly.starly.d.ts +0 -148
- /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { FidelityReport } from "./luals-fidelity";
|
|
4
|
+
import { type MarkdownDoc, parseMarkdownApi } from "./parse-markdown-api";
|
|
5
|
+
import {
|
|
6
|
+
computeScriptApiFidelity,
|
|
7
|
+
type FetchText,
|
|
8
|
+
loadTypeResolver,
|
|
9
|
+
type ScriptApiDoc,
|
|
10
|
+
type TypeResolver,
|
|
11
|
+
} from "./sync-script-api-types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The markdown ingestion front-end: a third `library-types` corpus mode beside
|
|
15
|
+
* the LuaLS front-end (`sync-luals-types.ts`) and the `.script_api` front-end
|
|
16
|
+
* (`sync-script-api-types.ts`). It reads a library's committed README/`.md`
|
|
17
|
+
* snapshot (`parseMarkdownApi`) into the shared ref-doc `doc` shape and routes it
|
|
18
|
+
* through the same emitter (`generateModuleDeclaration`) and fidelity machinery.
|
|
19
|
+
*
|
|
20
|
+
* The README documents its API under the library's own require alias (e.g.
|
|
21
|
+
* `camera.` for defold-orthographic); each target pins the `namespace` this repo
|
|
22
|
+
* publishes it under, so the parsed doc is *retargeted* onto that namespace
|
|
23
|
+
* before emitting — the goldens are named for the target's `namespace` verbatim
|
|
24
|
+
* (`generated/<namespace>.d.ts` — so `orthographic` lands at
|
|
25
|
+
* `generated/orthographic.d.ts`), keeping the file layout uniform with druid.
|
|
26
|
+
*
|
|
27
|
+
* A markdown parse is likely **lower fidelity** than the hand-written ts-defold
|
|
28
|
+
* `.d.ts` it would replace (a README rarely documents constants or every
|
|
29
|
+
* overload), so each cutover is gated: `compareFidelityToTsDefold` reports the
|
|
30
|
+
* member surface lost versus the retired ts-defold declaration, and the target's
|
|
31
|
+
* recorded `decision` (`go` / `no-go`) must match. The parser + front-end land as
|
|
32
|
+
* the reusable foundation either way; only the per-library cutover is gated.
|
|
33
|
+
*/
|
|
34
|
+
export interface MarkdownTarget {
|
|
35
|
+
repo: string;
|
|
36
|
+
ref: string;
|
|
37
|
+
markdown: string;
|
|
38
|
+
moduleId: string;
|
|
39
|
+
namespace: string;
|
|
40
|
+
generated: string;
|
|
41
|
+
apiDoc: string;
|
|
42
|
+
// Defaults to `fidelity/<namespace>.json` when omitted.
|
|
43
|
+
fidelity: string;
|
|
44
|
+
// SPDX-style license id, surfaced by the docs-site provenance block. Optional
|
|
45
|
+
// in the config; defaults to "".
|
|
46
|
+
license: string;
|
|
47
|
+
// The recorded fidelity go/no-go decision for this target. `no-go` keeps the
|
|
48
|
+
// library ts-defold-sourced; `go` cuts it over. Optional until the gate runs.
|
|
49
|
+
decision?: "go" | "no-go";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MarkdownTargets {
|
|
53
|
+
targets: MarkdownTarget[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const REQUIRED_FIELDS = [
|
|
57
|
+
"repo",
|
|
58
|
+
"ref",
|
|
59
|
+
"markdown",
|
|
60
|
+
"moduleId",
|
|
61
|
+
"namespace",
|
|
62
|
+
"generated",
|
|
63
|
+
"apiDoc",
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read `markdown-targets.json`, validate every required field per entry, and fill
|
|
68
|
+
* optional defaults (`fidelity` → `fidelity/<namespace>.json`, `license` → "").
|
|
69
|
+
* Throws on the first missing field naming both the field and the offending entry
|
|
70
|
+
* (its `moduleId`, or its index when `moduleId` itself is absent) — the loud-fail
|
|
71
|
+
* discipline `readScriptApiTargets`/`readLualsTargets` use. No network.
|
|
72
|
+
*/
|
|
73
|
+
export function readMarkdownTargets(packageRoot: string): MarkdownTarget[] {
|
|
74
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, "markdown-targets.json"), "utf8")) as {
|
|
75
|
+
targets: Partial<MarkdownTarget>[];
|
|
76
|
+
};
|
|
77
|
+
return parsed.targets.map((entry, index) => {
|
|
78
|
+
const label = typeof entry.moduleId === "string" ? entry.moduleId : `index ${index}`;
|
|
79
|
+
for (const field of REQUIRED_FIELDS) {
|
|
80
|
+
if (entry[field] === undefined) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`markdown-targets.json: entry ${label} is missing required field "${field}".`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
repo: entry.repo as string,
|
|
88
|
+
ref: entry.ref as string,
|
|
89
|
+
markdown: entry.markdown as string,
|
|
90
|
+
moduleId: entry.moduleId as string,
|
|
91
|
+
namespace: entry.namespace as string,
|
|
92
|
+
generated: entry.generated as string,
|
|
93
|
+
apiDoc: entry.apiDoc as string,
|
|
94
|
+
fidelity: entry.fidelity ?? `fidelity/${entry.namespace as string}.json`,
|
|
95
|
+
license: entry.license ?? "",
|
|
96
|
+
...(entry.decision !== undefined ? { decision: entry.decision } : {}),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A GitHub repo URL reduced to the bare `<owner>/<repo>` slug. */
|
|
102
|
+
function repoSlug(repo: string): string {
|
|
103
|
+
return repo
|
|
104
|
+
.replace(/^https:\/\/github\.com\//, "")
|
|
105
|
+
.replace(/\.git$/, "")
|
|
106
|
+
.replace(/\/$/, "");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function rawUrl(target: MarkdownTarget): string {
|
|
110
|
+
return `https://raw.githubusercontent.com/${repoSlug(target.repo)}/${target.ref}/${target.markdown}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fixturePath(packageRoot: string, target: MarkdownTarget): string {
|
|
114
|
+
return join(packageRoot, "fixtures/markdown", `${target.moduleId}.md`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Snapshot the pinned README/`.md` into `fixtures/markdown/<moduleId>.md` via the
|
|
119
|
+
* raw-content URL. Snapshot only — no parse. The `fetchText` seam keeps the pass
|
|
120
|
+
* offline-testable; only the CLI `--fetch` arm wires the real network.
|
|
121
|
+
*/
|
|
122
|
+
export async function fetchMarkdownFixture(
|
|
123
|
+
packageRoot: string,
|
|
124
|
+
target: MarkdownTarget,
|
|
125
|
+
seams: { fetchText: FetchText },
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const text = await seams.fetchText(rawUrl(target));
|
|
128
|
+
const dest = fixturePath(packageRoot, target);
|
|
129
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
130
|
+
writeFileSync(dest, text);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface RegenModule {
|
|
134
|
+
generateModuleDeclaration: (entry: {
|
|
135
|
+
namespace: string;
|
|
136
|
+
doc: unknown;
|
|
137
|
+
outFile: string;
|
|
138
|
+
importsFrom?: string;
|
|
139
|
+
moduleId?: string;
|
|
140
|
+
}) => { contents: string; dropped: string[] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// orthographic references only ambient engine handles (`hash`/`url`/`vmath.*`),
|
|
144
|
+
// which the module-wrapped form emits as globals, so this import never reaches
|
|
145
|
+
// the golden; it mirrors the script_api front-end for the day a markdown target
|
|
146
|
+
// needs a core-types import.
|
|
147
|
+
const MARKDOWN_CORE_TYPES_IMPORT = "../src/core-types";
|
|
148
|
+
|
|
149
|
+
async function loadGenerate(
|
|
150
|
+
packageRoot: string,
|
|
151
|
+
): Promise<RegenModule["generateModuleDeclaration"]> {
|
|
152
|
+
const typesRoot = join(packageRoot, "..", "types");
|
|
153
|
+
const regen = (await import(join(typesRoot, "scripts", "regen.ts"))) as RegenModule;
|
|
154
|
+
return regen.generateModuleDeclaration;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseFixtureDoc(packageRoot: string, target: MarkdownTarget): MarkdownDoc {
|
|
158
|
+
const text = readFileSync(fixturePath(packageRoot, target), "utf8");
|
|
159
|
+
return parseMarkdownApi(text, target.moduleId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Retarget the parsed doc onto the pinned publish namespace: rewrite each
|
|
164
|
+
* element's leading `<readmePrefix>.` to `<namespace>.` and set `info.namespace`.
|
|
165
|
+
* The README's own alias (`camera`) becomes the target namespace (`orthographic`).
|
|
166
|
+
*/
|
|
167
|
+
export function retargetDoc(doc: MarkdownDoc, namespace: string): MarkdownDoc {
|
|
168
|
+
const oldPrefix = `${doc.info.namespace}.`;
|
|
169
|
+
return {
|
|
170
|
+
info: { ...doc.info, namespace },
|
|
171
|
+
elements: doc.elements.map((element) => ({
|
|
172
|
+
...element,
|
|
173
|
+
name: element.name.startsWith(oldPrefix)
|
|
174
|
+
? `${namespace}.${element.name.slice(oldPrefix.length)}`
|
|
175
|
+
: element.name,
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function retargetedFixtureDoc(packageRoot: string, target: MarkdownTarget): MarkdownDoc {
|
|
181
|
+
return retargetDoc(parseFixtureDoc(packageRoot, target), target.namespace);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `parseMarkdownApi` -> retarget -> `generateModuleDeclaration`. Returns an
|
|
186
|
+
* importable module keyed by `moduleId` (`declare module '<moduleId>'`).
|
|
187
|
+
*/
|
|
188
|
+
export async function emitMarkdownDeclaration(
|
|
189
|
+
packageRoot: string,
|
|
190
|
+
target: MarkdownTarget,
|
|
191
|
+
): Promise<string> {
|
|
192
|
+
const generate = await loadGenerate(packageRoot);
|
|
193
|
+
const doc = retargetedFixtureDoc(packageRoot, target);
|
|
194
|
+
const { contents } = generate({
|
|
195
|
+
namespace: target.namespace,
|
|
196
|
+
doc,
|
|
197
|
+
outFile: `${target.moduleId}.d.ts`,
|
|
198
|
+
importsFrom: MARKDOWN_CORE_TYPES_IMPORT,
|
|
199
|
+
moduleId: target.moduleId,
|
|
200
|
+
});
|
|
201
|
+
return contents;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The api-doc golden is the retargeted ref-doc `doc` itself, pretty-printed. */
|
|
205
|
+
export function lowerMarkdownApiDoc(packageRoot: string, target: MarkdownTarget): string {
|
|
206
|
+
const doc = retargetedFixtureDoc(packageRoot, target);
|
|
207
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Type tokens a human triaged as acceptably lossy for a markdown cutover. The
|
|
211
|
+
// allowlist is drained — every token orthographic's README uses now maps to a
|
|
212
|
+
// real TS type — but the mechanism stays: a new sibling's unexpected token
|
|
213
|
+
// loud-fails at regen until it is mapped or deliberately added here.
|
|
214
|
+
const KNOWN_LOSSY_TOKENS = new Set<string>([]);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* markdown-scoped wrapper over the shared `computeScriptApiFidelity`. After the
|
|
218
|
+
* report is built, any `unknownToken` outside `KNOWN_LOSSY_TOKENS` loud-fails —
|
|
219
|
+
* a brand-new library's unclassified token must not be swallowed as `unknown` at
|
|
220
|
+
* regen time. The shared `computeScriptApiFidelity` is left untouched so the
|
|
221
|
+
* script_api/luals goldens (which legitimately carry unknown tokens) stay green.
|
|
222
|
+
*/
|
|
223
|
+
export function computeMarkdownFidelity(
|
|
224
|
+
namespace: string,
|
|
225
|
+
doc: MarkdownDoc,
|
|
226
|
+
resolver: TypeResolver,
|
|
227
|
+
): FidelityReport {
|
|
228
|
+
const report = computeScriptApiFidelity(namespace, doc as unknown as ScriptApiDoc, resolver);
|
|
229
|
+
const unmappable = report.unknownTokens.filter((token) => !KNOWN_LOSSY_TOKENS.has(token));
|
|
230
|
+
if (unmappable.length > 0) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`markdown fidelity [${namespace}]: unmappable type token(s) ${JSON.stringify(unmappable)} — resolve them or add to KNOWN_LOSSY_TOKENS`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return report;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function buildMarkdownFidelity(
|
|
239
|
+
packageRoot: string,
|
|
240
|
+
target: MarkdownTarget,
|
|
241
|
+
): Promise<FidelityReport> {
|
|
242
|
+
const resolver = await loadTypeResolver(packageRoot);
|
|
243
|
+
const doc = retargetedFixtureDoc(packageRoot, target);
|
|
244
|
+
return computeMarkdownFidelity(target.namespace, doc, resolver);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The result of comparing an emitted markdown surface against the ts-defold
|
|
248
|
+
* `.d.ts` it would replace. `decision` is `no-go` whenever a ts-defold member is
|
|
249
|
+
* absent from the markdown emit (a missing member), **or** a member both surfaces
|
|
250
|
+
* share was downgraded to `unknown` by the markdown emit (a lost type), **or** a
|
|
251
|
+
* shared member kept its name but lost part of its signature (dropped parameters,
|
|
252
|
+
* or a non-`void` return collapsed to `void`), **or** a shared member kept its
|
|
253
|
+
* arity but emitted an optional ts-defold parameter as required, **or** the
|
|
254
|
+
* ts-defold side presents no comparable member at all (an opaque surface), which
|
|
255
|
+
* would otherwise leave every other term vacuously empty and read as `go`. */
|
|
256
|
+
export interface FidelityComparison {
|
|
257
|
+
tsDefoldMembers: string[];
|
|
258
|
+
markdownMembers: string[];
|
|
259
|
+
missingMembers: string[];
|
|
260
|
+
addedMembers: string[];
|
|
261
|
+
downgradedMembers: string[];
|
|
262
|
+
signatureLossMembers: string[];
|
|
263
|
+
optionalityLossMembers: string[];
|
|
264
|
+
opaqueTsDefoldSurface: boolean;
|
|
265
|
+
decision: "go" | "no-go";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface TsDefoldMember {
|
|
269
|
+
kind: "function" | "const";
|
|
270
|
+
signature: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Optional `export`, then `function`/`const`, then the member name. Bare (no
|
|
274
|
+
// `export`) declarations are valid inside `declare module` and the markdown
|
|
275
|
+
// emitter produces them, so `export` must not be required.
|
|
276
|
+
const MEMBER_DECL = /(?:export\s+)?(function|const)\s+([A-Za-z_]\w*)/g;
|
|
277
|
+
|
|
278
|
+
/** Strip block and line comments so keyword-shaped prose inside a doc comment
|
|
279
|
+
* (e.g. "This function is called…") never latches onto `MEMBER_DECL`, and so a
|
|
280
|
+
* `unknown` mentioned in JSDoc never reads as a real type downgrade. A single
|
|
281
|
+
* left-to-right scan decides at each `/` which form opens, so `//*` — a section
|
|
282
|
+
* marker some bindings use — reads as a line comment rather than as a block
|
|
283
|
+
* comment running to the next block-comment terminator many declarations
|
|
284
|
+
* later. */
|
|
285
|
+
function stripComments(dts: string): string {
|
|
286
|
+
let out = "";
|
|
287
|
+
let index = 0;
|
|
288
|
+
while (index < dts.length) {
|
|
289
|
+
if (dts[index] === "/" && dts[index + 1] === "/") {
|
|
290
|
+
const end = dts.indexOf("\n", index);
|
|
291
|
+
if (end === -1) break;
|
|
292
|
+
index = end;
|
|
293
|
+
} else if (dts[index] === "/" && dts[index + 1] === "*") {
|
|
294
|
+
const end = dts.indexOf("*/", index + 2);
|
|
295
|
+
index = end === -1 ? dts.length : end + 2;
|
|
296
|
+
} else {
|
|
297
|
+
out += dts[index];
|
|
298
|
+
index++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Scan forward from `start` to the index of the first `end` character that sits
|
|
305
|
+
* at group depth zero, so a delimiter inside `()`/`[]`/`{}`/`<>` never
|
|
306
|
+
* terminates the construct being read. `-1` when the source runs out. */
|
|
307
|
+
function findAtDepthZero(src: string, start: number, end: string): number {
|
|
308
|
+
let depth = 0;
|
|
309
|
+
for (let i = start; i < src.length; i++) {
|
|
310
|
+
if (src[i] === end && depth === 0) return i;
|
|
311
|
+
if (opensGroup(src[i])) depth++;
|
|
312
|
+
else if (closesGroup(src, i)) depth--;
|
|
313
|
+
}
|
|
314
|
+
return -1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Every `interface Name { … }` body and `type Name = …` right-hand side the
|
|
318
|
+
* source declares, keyed by name. Both forms are read by brace/angle balancing
|
|
319
|
+
* rather than by line, so a multiline body or a generic argument list carrying
|
|
320
|
+
* its own delimiters stays intact. */
|
|
321
|
+
function declaredShapes(src: string): Map<string, string> {
|
|
322
|
+
const shapes = new Map<string, string>();
|
|
323
|
+
const decl = /\b(interface|type)\s+([A-Za-z_]\w*)/g;
|
|
324
|
+
let match = decl.exec(src);
|
|
325
|
+
while (match !== null) {
|
|
326
|
+
const name = match[2] as string;
|
|
327
|
+
if (match[1] === "interface") {
|
|
328
|
+
const open = src.indexOf("{", decl.lastIndex);
|
|
329
|
+
if (open !== -1) {
|
|
330
|
+
const close = findAtDepthZero(src, open + 1, "}");
|
|
331
|
+
if (close !== -1 && !shapes.has(name)) shapes.set(name, src.slice(open, close + 1));
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
const assign = findAtDepthZero(src, decl.lastIndex, "=");
|
|
335
|
+
if (assign !== -1) {
|
|
336
|
+
const end = findAtDepthZero(src, assign + 1, ";");
|
|
337
|
+
const body = src.slice(assign + 1, end === -1 ? src.length : end);
|
|
338
|
+
if (!shapes.has(name)) shapes.set(name, body);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
match = decl.exec(src);
|
|
342
|
+
}
|
|
343
|
+
return shapes;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Split a type expression on its top-level `&`, so an intersection arm keeps
|
|
347
|
+
* any `&` nested inside its own generic arguments. */
|
|
348
|
+
function splitIntersection(expression: string): string[] {
|
|
349
|
+
const arms: string[] = [];
|
|
350
|
+
let depth = 0;
|
|
351
|
+
let current = "";
|
|
352
|
+
for (let i = 0; i < expression.length; i++) {
|
|
353
|
+
const ch = expression[i] as string;
|
|
354
|
+
if (opensGroup(ch)) depth++;
|
|
355
|
+
else if (closesGroup(expression, i)) depth--;
|
|
356
|
+
if (ch === "&" && depth === 0) {
|
|
357
|
+
arms.push(current);
|
|
358
|
+
current = "";
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
current += ch;
|
|
362
|
+
}
|
|
363
|
+
arms.push(current);
|
|
364
|
+
return arms.map((arm) => arm.trim()).filter((arm) => arm !== "");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The inner type of a `Readonly<X>` wrapper, or `undefined` when the
|
|
368
|
+
* expression is not one. `Readonly<>` only restates members as read-only, so
|
|
369
|
+
* the module surface underneath it is the same surface. */
|
|
370
|
+
function readonlyInner(expression: string): string | undefined {
|
|
371
|
+
if (!expression.startsWith("Readonly<") || !expression.endsWith(">")) return undefined;
|
|
372
|
+
const inner = expression.slice("Readonly<".length, -1);
|
|
373
|
+
return findAtDepthZero(inner, 0, ">") === -1 ? inner : undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The members of an object body (`{ … }`), split on its top-level `;`/`,`
|
|
377
|
+
* separators. A member with a call signature normalizes to a `function`
|
|
378
|
+
* declaration and a field to a `const` one, so `signatureShape`,
|
|
379
|
+
* `isOptionalParameter` and the `unknown` test read a resolved member exactly
|
|
380
|
+
* as they read a flat one. */
|
|
381
|
+
function objectBodyMembers(body: string): Map<string, TsDefoldMember> {
|
|
382
|
+
const members = new Map<string, TsDefoldMember>();
|
|
383
|
+
const inner = body.trim().replace(/^\{/, "").replace(/\}$/, "");
|
|
384
|
+
const segments: string[] = [];
|
|
385
|
+
let depth = 0;
|
|
386
|
+
let current = "";
|
|
387
|
+
for (let i = 0; i < inner.length; i++) {
|
|
388
|
+
const ch = inner[i] as string;
|
|
389
|
+
if (opensGroup(ch)) depth++;
|
|
390
|
+
else if (closesGroup(inner, i)) depth--;
|
|
391
|
+
if ((ch === ";" || ch === ",") && depth === 0) {
|
|
392
|
+
segments.push(current);
|
|
393
|
+
current = "";
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
current += ch;
|
|
397
|
+
}
|
|
398
|
+
segments.push(current);
|
|
399
|
+
for (const segment of segments) {
|
|
400
|
+
const text = segment
|
|
401
|
+
.trim()
|
|
402
|
+
.replace(/^readonly\s+/, "")
|
|
403
|
+
.replace(/\s+/g, " ");
|
|
404
|
+
const head = /^([A-Za-z_]\w*)\s*\??\s*([(:])/.exec(text);
|
|
405
|
+
if (head === null) continue;
|
|
406
|
+
const name = head[1] as string;
|
|
407
|
+
if (members.has(name)) continue;
|
|
408
|
+
if (head[2] === "(") {
|
|
409
|
+
const open = text.indexOf("(");
|
|
410
|
+
const close = findAtDepthZero(text, open + 1, ")");
|
|
411
|
+
if (close === -1) continue;
|
|
412
|
+
const parameters = text.slice(open + 1, close).trim();
|
|
413
|
+
const tail = text.slice(close + 1).trim();
|
|
414
|
+
const returnType = tail.startsWith(":") ? tail.slice(1).trim() : "";
|
|
415
|
+
members.set(name, {
|
|
416
|
+
kind: "function",
|
|
417
|
+
signature: `function ${name}(${parameters})${returnType === "" ? "" : `: ${returnType}`};`,
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
const type = text.slice(text.indexOf(":") + 1).trim();
|
|
421
|
+
members.set(name, { kind: "const", signature: `const ${name}: ${type};` });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return members;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Resolve a type expression to the members it publishes: an inline object body,
|
|
428
|
+
* a named `interface`/`type` the source declares, a `Readonly<>` wrapper, or the
|
|
429
|
+
* union of an intersection's arms. An arm that names no object shape — an
|
|
430
|
+
* undeclared token, a `LuaMap<…>`, a primitive — contributes nothing instead of
|
|
431
|
+
* failing, so a partly-opaque handle still yields the part that is readable. */
|
|
432
|
+
function resolveShapeMembers(
|
|
433
|
+
expression: string,
|
|
434
|
+
shapes: Map<string, string>,
|
|
435
|
+
seen: Set<string> = new Set(),
|
|
436
|
+
): Map<string, TsDefoldMember> {
|
|
437
|
+
const text = expression.trim();
|
|
438
|
+
const arms = splitIntersection(text);
|
|
439
|
+
if (arms.length > 1) {
|
|
440
|
+
const merged = new Map<string, TsDefoldMember>();
|
|
441
|
+
for (const arm of arms) {
|
|
442
|
+
for (const [name, member] of resolveShapeMembers(arm, shapes, seen)) {
|
|
443
|
+
if (!merged.has(name)) merged.set(name, member);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return merged;
|
|
447
|
+
}
|
|
448
|
+
if (text.startsWith("{") && text.endsWith("}")) return objectBodyMembers(text);
|
|
449
|
+
const unwrapped = readonlyInner(text);
|
|
450
|
+
if (unwrapped !== undefined) return resolveShapeMembers(unwrapped, shapes, seen);
|
|
451
|
+
if (!/^[A-Za-z_]\w*$/.test(text) || seen.has(text)) return new Map();
|
|
452
|
+
const shape = shapes.get(text);
|
|
453
|
+
if (shape === undefined) return new Map();
|
|
454
|
+
return resolveShapeMembers(shape, shapes, new Set([...seen, text]));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Extract every top-level `function`/`const` member of a module `.d.ts` — both
|
|
459
|
+
* `export`ed and bare — with its declaration text (through the terminating
|
|
460
|
+
* top-level `;`, balancing `()`/`{}` for multiline signatures and object-typed
|
|
461
|
+
* consts). The emitted markdown module uses bare `function`, so a single
|
|
462
|
+
* extractor serves both sides of the comparison.
|
|
463
|
+
*
|
|
464
|
+
* A module that publishes through `export = handle` is read through the handle's
|
|
465
|
+
* shape instead: the module *is* that object, so the shape's members are the
|
|
466
|
+
* module's own flat members — the same flattening `emitLibraryDeclarations`
|
|
467
|
+
* performs on the markdown side. A handle whose type resolves to no member at
|
|
468
|
+
* all is left standing, so an uncomparable surface still presents as one.
|
|
469
|
+
*/
|
|
470
|
+
export function tsDefoldSurface(dts: string): Map<string, TsDefoldMember> {
|
|
471
|
+
const src = stripComments(dts);
|
|
472
|
+
const members = new Map<string, TsDefoldMember>();
|
|
473
|
+
MEMBER_DECL.lastIndex = 0;
|
|
474
|
+
let match = MEMBER_DECL.exec(src);
|
|
475
|
+
while (match !== null) {
|
|
476
|
+
const kind = match[1] as "function" | "const";
|
|
477
|
+
const name = match[2] as string;
|
|
478
|
+
let index = MEMBER_DECL.lastIndex;
|
|
479
|
+
let parens = 0;
|
|
480
|
+
let braces = 0;
|
|
481
|
+
while (index < src.length) {
|
|
482
|
+
const ch = src[index];
|
|
483
|
+
if (ch === "(") parens++;
|
|
484
|
+
else if (ch === ")") parens--;
|
|
485
|
+
else if (ch === "{") braces++;
|
|
486
|
+
else if (ch === "}") braces--;
|
|
487
|
+
else if (ch === ";" && parens === 0 && braces === 0) break;
|
|
488
|
+
index++;
|
|
489
|
+
}
|
|
490
|
+
const signature = src
|
|
491
|
+
.slice(match.index, index + 1)
|
|
492
|
+
.replace(/\s+/g, " ")
|
|
493
|
+
.trim();
|
|
494
|
+
if (!members.has(name)) members.set(name, { kind, signature });
|
|
495
|
+
MEMBER_DECL.lastIndex = index + 1;
|
|
496
|
+
match = MEMBER_DECL.exec(src);
|
|
497
|
+
}
|
|
498
|
+
const handle = /export\s*=\s*([A-Za-z_]\w*)\s*;/.exec(src)?.[1];
|
|
499
|
+
const handleEntry = handle === undefined ? undefined : members.get(handle);
|
|
500
|
+
if (handle !== undefined && handleEntry?.kind === "const") {
|
|
501
|
+
const declaredType = handleEntry.signature.slice(handleEntry.signature.indexOf(":") + 1);
|
|
502
|
+
const resolved = resolveShapeMembers(declaredType.replace(/;\s*$/, ""), declaredShapes(src));
|
|
503
|
+
if (resolved.size > 0) {
|
|
504
|
+
members.delete(handle);
|
|
505
|
+
for (const [name, member] of resolved) {
|
|
506
|
+
if (!members.has(name)) members.set(name, member);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return members;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** The top-level member locals of a module `.d.ts`, sorted. */
|
|
514
|
+
export function tsDefoldMembers(dts: string): string[] {
|
|
515
|
+
return [...tsDefoldSurface(dts).keys()].sort();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Whether a closing bracket at `index` really closes a type group. `>` is the
|
|
519
|
+
* only ambiguous one: in `(x: string) => void` it belongs to the arrow, not to a
|
|
520
|
+
* generic argument list. */
|
|
521
|
+
function closesGroup(text: string, index: number): boolean {
|
|
522
|
+
const ch = text[index];
|
|
523
|
+
if (ch === ")" || ch === "]" || ch === "}") return true;
|
|
524
|
+
return ch === ">" && text[index - 1] !== "=";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function opensGroup(ch: string | undefined): boolean {
|
|
528
|
+
return ch === "(" || ch === "[" || ch === "{" || ch === "<";
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Split a parameter list on its top-level commas, so a comma inside a
|
|
532
|
+
* parameter's own type (`Record<string, number>`, `[a, b]`) never reads as a
|
|
533
|
+
* separator. */
|
|
534
|
+
function splitParameters(inner: string): string[] {
|
|
535
|
+
const parts: string[] = [];
|
|
536
|
+
let depth = 0;
|
|
537
|
+
let current = "";
|
|
538
|
+
for (let i = 0; i < inner.length; i++) {
|
|
539
|
+
const ch = inner[i] as string;
|
|
540
|
+
if (opensGroup(ch)) depth++;
|
|
541
|
+
else if (closesGroup(inner, i)) depth--;
|
|
542
|
+
if (ch === "," && depth === 0) {
|
|
543
|
+
parts.push(current);
|
|
544
|
+
current = "";
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
current += ch;
|
|
548
|
+
}
|
|
549
|
+
parts.push(current);
|
|
550
|
+
return parts.map((part) => part.trim()).filter((part) => part !== "");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Whether a parameter slot is declared optional. Optionality is a `?` on the
|
|
554
|
+
* parameter *name*; a `?` anywhere in the parameter's own type (`cb: (x?: number)
|
|
555
|
+
* => void`) belongs to that type, not to the slot. */
|
|
556
|
+
function isOptionalParameter(parameter: string): boolean {
|
|
557
|
+
return /^[A-Za-z_]\w*\?\s*:/.test(parameter.trim());
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
interface SignatureShape {
|
|
561
|
+
parameters: string[];
|
|
562
|
+
returnType: string;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The top-level parameter list and return type of a `function` declaration.
|
|
566
|
+
* `undefined` when the text carries no balanced parameter list at all — a `const`
|
|
567
|
+
* declaration, or a signature the extractor truncated. */
|
|
568
|
+
function signatureShape(signature: string): SignatureShape | undefined {
|
|
569
|
+
const open = signature.indexOf("(");
|
|
570
|
+
if (open === -1) return undefined;
|
|
571
|
+
let depth = 0;
|
|
572
|
+
let close = -1;
|
|
573
|
+
for (let i = open; i < signature.length; i++) {
|
|
574
|
+
if (opensGroup(signature[i])) depth++;
|
|
575
|
+
else if (closesGroup(signature, i)) {
|
|
576
|
+
depth--;
|
|
577
|
+
if (depth === 0) {
|
|
578
|
+
close = i;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (close === -1) return undefined;
|
|
584
|
+
const tail = signature
|
|
585
|
+
.slice(close + 1)
|
|
586
|
+
.trim()
|
|
587
|
+
.replace(/;\s*$/, "")
|
|
588
|
+
.trim();
|
|
589
|
+
return {
|
|
590
|
+
parameters: splitParameters(signature.slice(open + 1, close)),
|
|
591
|
+
returnType: tail.startsWith(":") ? tail.slice(1).trim() : "",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Compare the emitted markdown `.d.ts` against the ts-defold `.d.ts` it would
|
|
597
|
+
* replace and derive the go/no-go decision. A ts-defold member absent from the
|
|
598
|
+
* markdown emit is a missing member; a shared member whose markdown signature
|
|
599
|
+
* introduced `unknown` the ts-defold declaration lacked is a type downgrade; a
|
|
600
|
+
* shared member that kept its name but dropped parameters, or collapsed a
|
|
601
|
+
* non-`void` return to `void`, is a signature loss; a shared member that kept
|
|
602
|
+
* every ts-defold parameter but emitted an optional one as required is an
|
|
603
|
+
* optionality loss. A ts-defold side whose only top-level declaration is its
|
|
604
|
+
* `export =` re-export handle — one whose type named no readable shape, since a
|
|
605
|
+
* handle that resolves is already flattened to its members — or which declares
|
|
606
|
+
* no top-level member at all is an opaque surface: there is nothing to compare
|
|
607
|
+
* against, so every other term is vacuously empty. Any of the five forces
|
|
608
|
+
* `no-go`.
|
|
609
|
+
*/
|
|
610
|
+
export function compareFidelityToTsDefold(
|
|
611
|
+
markdownEmittedDts: string,
|
|
612
|
+
tsDefoldDts: string,
|
|
613
|
+
): FidelityComparison {
|
|
614
|
+
const tsMap = tsDefoldSurface(tsDefoldDts);
|
|
615
|
+
const mdMap = tsDefoldSurface(markdownEmittedDts);
|
|
616
|
+
const tsMembers = [...tsMap.keys()].sort();
|
|
617
|
+
const mdMembers = [...mdMap.keys()].sort();
|
|
618
|
+
const missingMembers = tsMembers.filter((name) => !mdMap.has(name));
|
|
619
|
+
const addedMembers = mdMembers.filter((name) => !tsMap.has(name));
|
|
620
|
+
const hasUnknown = (member: TsDefoldMember | undefined): boolean =>
|
|
621
|
+
member !== undefined && /\bunknown\b/.test(member.signature);
|
|
622
|
+
const downgradedMembers = tsMembers.filter(
|
|
623
|
+
(name) => hasUnknown(mdMap.get(name)) && !hasUnknown(tsMap.get(name)),
|
|
624
|
+
);
|
|
625
|
+
const signatureLossMembers = tsMembers.filter((name) => {
|
|
626
|
+
const ts = tsMap.get(name);
|
|
627
|
+
const md = mdMap.get(name);
|
|
628
|
+
if (ts === undefined || md === undefined) return false;
|
|
629
|
+
if (ts.kind !== "function" || md.kind !== "function") return false;
|
|
630
|
+
const tsShape = signatureShape(ts.signature);
|
|
631
|
+
const mdShape = signatureShape(md.signature);
|
|
632
|
+
if (tsShape === undefined || mdShape === undefined) return false;
|
|
633
|
+
if (mdShape.parameters.length < tsShape.parameters.length) return true;
|
|
634
|
+
// An unannotated ts-defold return has nothing to lose against.
|
|
635
|
+
return (
|
|
636
|
+
mdShape.returnType === "void" && tsShape.returnType !== "void" && tsShape.returnType !== ""
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
const optionalityLossMembers = tsMembers.filter((name) => {
|
|
640
|
+
const ts = tsMap.get(name);
|
|
641
|
+
const md = mdMap.get(name);
|
|
642
|
+
if (ts === undefined || md === undefined) return false;
|
|
643
|
+
if (ts.kind !== "function" || md.kind !== "function") return false;
|
|
644
|
+
const tsShape = signatureShape(ts.signature);
|
|
645
|
+
const mdShape = signatureShape(md.signature);
|
|
646
|
+
if (tsShape === undefined || mdShape === undefined) return false;
|
|
647
|
+
return tsShape.parameters.some((parameter, index) => {
|
|
648
|
+
const counterpart = mdShape.parameters[index];
|
|
649
|
+
return (
|
|
650
|
+
counterpart !== undefined &&
|
|
651
|
+
isOptionalParameter(parameter) &&
|
|
652
|
+
!isOptionalParameter(counterpart)
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
// Reached only by a handle whose type resolved to nothing — a resolved one is
|
|
657
|
+
// no longer in the surface under its own name. `tsDefoldMembers` keeps
|
|
658
|
+
// reporting what the extractor really saw; only the predicate discounts the
|
|
659
|
+
// handle, so a record can still quote it as the evidence for the opacity.
|
|
660
|
+
const reExportHandle = /export\s*=\s*(\w+)\s*;/.exec(tsDefoldDts)?.[1];
|
|
661
|
+
const comparableTsMembers = tsMembers.filter((name) => name !== reExportHandle);
|
|
662
|
+
const opaqueTsDefoldSurface = comparableTsMembers.length === 0;
|
|
663
|
+
return {
|
|
664
|
+
tsDefoldMembers: tsMembers,
|
|
665
|
+
markdownMembers: mdMembers,
|
|
666
|
+
missingMembers,
|
|
667
|
+
addedMembers,
|
|
668
|
+
downgradedMembers,
|
|
669
|
+
signatureLossMembers,
|
|
670
|
+
optionalityLossMembers,
|
|
671
|
+
opaqueTsDefoldSurface,
|
|
672
|
+
decision:
|
|
673
|
+
missingMembers.length === 0 &&
|
|
674
|
+
downgradedMembers.length === 0 &&
|
|
675
|
+
signatureLossMembers.length === 0 &&
|
|
676
|
+
optionalityLossMembers.length === 0 &&
|
|
677
|
+
!opaqueTsDefoldSurface
|
|
678
|
+
? "go"
|
|
679
|
+
: "no-go",
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Evaluate one cutover candidate without writing anything: emit the markdown
|
|
685
|
+
* surface in memory and compare it against the `fixtures/ts-defold/<moduleId>.d.ts`
|
|
686
|
+
* it would replace.
|
|
687
|
+
*
|
|
688
|
+
* This is the entry point for a library whose markdown `namespace` *equals* its
|
|
689
|
+
* live `moduleId` (defold-input, monarch), where the canonical
|
|
690
|
+
* `generated`/`api-doc`/`fidelity` paths a registered target would write are the
|
|
691
|
+
* very files the live ts-defold module already owns. Registering a no-go target
|
|
692
|
+
* to obtain its comparison would overwrite them in place; passing an unregistered
|
|
693
|
+
* in-memory target here computes the same decision and leaves them untouched.
|
|
694
|
+
*
|
|
695
|
+
* `snapshot` overrides the compared-against surface, package-root-relative. A
|
|
696
|
+
* library that severed its ts-defold dependency (forking its surface into the
|
|
697
|
+
* authored lane) has no `fixtures/ts-defold/` entry left, but its recorded
|
|
698
|
+
* markdown verdict is still owed a comparison: a verbatim fork is byte-identical
|
|
699
|
+
* to the retired snapshot, so pointing this at the vendored copy reproduces the
|
|
700
|
+
* decision exactly.
|
|
701
|
+
*/
|
|
702
|
+
export async function evaluateMarkdownCandidate(
|
|
703
|
+
packageRoot: string,
|
|
704
|
+
target: MarkdownTarget,
|
|
705
|
+
snapshot?: string,
|
|
706
|
+
): Promise<FidelityComparison & { emitted: string }> {
|
|
707
|
+
const emitted = await emitMarkdownDeclaration(packageRoot, target);
|
|
708
|
+
const tsDefold = readFileSync(
|
|
709
|
+
join(packageRoot, snapshot ?? join("fixtures/ts-defold", `${target.moduleId}.d.ts`)),
|
|
710
|
+
"utf8",
|
|
711
|
+
);
|
|
712
|
+
return { emitted, ...compareFidelityToTsDefold(emitted, tsDefold) };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const defaultFetchText: FetchText = async (url) => {
|
|
716
|
+
const res = await fetch(url);
|
|
717
|
+
if (!res.ok) {
|
|
718
|
+
throw new Error(`fetch failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
719
|
+
}
|
|
720
|
+
return res.text();
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
if (import.meta.main) {
|
|
724
|
+
const root = join(import.meta.dir, "..");
|
|
725
|
+
const argv = process.argv.slice(2);
|
|
726
|
+
if (argv.includes("--fetch")) {
|
|
727
|
+
for (const target of readMarkdownTargets(root)) {
|
|
728
|
+
await fetchMarkdownFixture(root, target, { fetchText: defaultFetchText });
|
|
729
|
+
console.log(`snapshotted ${target.moduleId} from ${repoSlug(target.repo)}@${target.ref}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (argv.includes("--emit")) {
|
|
733
|
+
for (const target of readMarkdownTargets(root)) {
|
|
734
|
+
const contents = await emitMarkdownDeclaration(root, target);
|
|
735
|
+
const dest = join(root, target.generated);
|
|
736
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
737
|
+
writeFileSync(dest, contents);
|
|
738
|
+
console.log(`emitted ${target.moduleId} -> ${target.generated}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (argv.includes("--api-doc")) {
|
|
742
|
+
for (const target of readMarkdownTargets(root)) {
|
|
743
|
+
const json = lowerMarkdownApiDoc(root, target);
|
|
744
|
+
const dest = join(root, target.apiDoc);
|
|
745
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
746
|
+
writeFileSync(dest, json);
|
|
747
|
+
console.log(`lowered ${target.moduleId} -> ${target.apiDoc}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (argv.includes("--fidelity")) {
|
|
751
|
+
for (const target of readMarkdownTargets(root)) {
|
|
752
|
+
const report = await buildMarkdownFidelity(root, target);
|
|
753
|
+
const dest = join(root, target.fidelity);
|
|
754
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
755
|
+
writeFileSync(dest, `${JSON.stringify(report, null, 2)}\n`);
|
|
756
|
+
console.log(
|
|
757
|
+
`${target.moduleId}: coverage ${(report.coverage * 100).toFixed(1)}% (${report.unknownFallbacks} unknown, ${report.undocumentedMembers} undocumented)`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|