@defold-typescript/types 0.1.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-targets.json +84 -0
- package/generated/b2d.d.ts +13 -0
- package/generated/buffer.d.ts +25 -0
- package/generated/builtin-messages.d.ts +42 -0
- package/generated/camera.d.ts +7 -0
- package/generated/collectionfactory.d.ts +17 -0
- package/generated/collectionproxy.d.ts +14 -0
- package/generated/crash.d.ts +31 -0
- package/generated/factory.d.ts +17 -0
- package/generated/go.d.ts +96 -0
- package/generated/graphics.d.ts +115 -0
- package/generated/gui.d.ts +245 -0
- package/generated/http.d.ts +8 -0
- package/generated/iac.d.ts +8 -0
- package/generated/iap.d.ts +16 -0
- package/generated/image.d.ts +16 -0
- package/generated/json.d.ts +11 -0
- package/generated/kinds/gui-script.d.ts +39 -0
- package/generated/kinds/render-script.d.ts +39 -0
- package/generated/kinds/script.d.ts +38 -0
- package/generated/label.d.ts +23 -0
- package/generated/liveupdate.d.ts +23 -0
- package/generated/model.d.ts +23 -0
- package/generated/msg.d.ts +12 -0
- package/generated/particlefx.d.ts +22 -0
- package/generated/physics.d.ts +47 -0
- package/generated/profiler.d.ts +28 -0
- package/generated/push.d.ts +15 -0
- package/generated/render.d.ts +55 -0
- package/generated/resource.d.ts +33 -0
- package/generated/socket.d.ts +25 -0
- package/generated/sound.d.ts +28 -0
- package/generated/sprite.d.ts +23 -0
- package/generated/sys.d.ts +37 -0
- package/generated/tilemap.d.ts +24 -0
- package/generated/timer.d.ts +12 -0
- package/generated/vmath.d.ts +63 -0
- package/generated/webview.d.ts +15 -0
- package/generated/window.d.ts +28 -0
- package/generated/zlib.d.ts +9 -0
- package/index.d.ts +63 -0
- package/package.json +46 -0
- package/scripts/doc-source.ts +100 -0
- package/scripts/fidelity-audit.ts +311 -0
- package/scripts/fidelity-baseline.json +282 -0
- package/scripts/materialize-version.ts +51 -0
- package/scripts/regen.ts +294 -0
- package/scripts/sync-api-docs.ts +375 -0
- package/src/api-doc.ts +168 -0
- package/src/core-types.ts +121 -0
- package/src/emit-dts.ts +754 -0
- package/src/emit-messages.ts +148 -0
- package/src/go-overloads.d.ts +35 -0
- package/src/index.ts +24 -0
- package/src/lifecycle.ts +81 -0
- package/src/msg-overloads.d.ts +21 -0
- package/src/publish-dts.ts +33 -0
- package/src/script-api.ts +95 -0
package/scripts/regen.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
|
|
4
|
+
import { parseDefoldApiDoc } from "../src/api-doc";
|
|
5
|
+
import { emitDeclarations } from "../src/emit-dts";
|
|
6
|
+
import { emitBuiltinMessages, parseMessagesDoc } from "../src/emit-messages";
|
|
7
|
+
import { wrapAsAmbientGlobal } from "../src/publish-dts";
|
|
8
|
+
import { type DownloadRefDoc, refDocCacheDir, resolveRefDoc } from "./doc-source";
|
|
9
|
+
import { type readZip, SYNC_MANIFEST, type SyncManifestEntry } from "./sync-api-docs";
|
|
10
|
+
|
|
11
|
+
export interface ApiTargetModule {
|
|
12
|
+
readonly namespace: string;
|
|
13
|
+
readonly fixture: string;
|
|
14
|
+
readonly outFile: string;
|
|
15
|
+
readonly skipFunctions?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A target sourced from a resolved ref-doc zip (resolved on demand, never
|
|
19
|
+
// pre-baked) versus the committed-fixture default (`null`).
|
|
20
|
+
export type ApiTargetSource = { readonly kind: "ref-doc"; readonly version: string } | null;
|
|
21
|
+
|
|
22
|
+
export interface ApiTarget {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly default?: boolean;
|
|
25
|
+
readonly fixturesDir: string;
|
|
26
|
+
readonly generatedDir: string;
|
|
27
|
+
readonly coreTypesImport: string;
|
|
28
|
+
readonly source?: ApiTargetSource;
|
|
29
|
+
readonly modules: readonly ApiTargetModule[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const REGISTRY_PATH = resolve(import.meta.dir, "..", "api-targets.json");
|
|
33
|
+
const PACKAGE_ROOT = resolve(import.meta.dir, "..");
|
|
34
|
+
|
|
35
|
+
export function loadApiTargets(registryPath: string = REGISTRY_PATH): ApiTarget[] {
|
|
36
|
+
const { targets } = JSON.parse(readFileSync(registryPath, "utf8")) as { targets: ApiTarget[] };
|
|
37
|
+
const defaults = targets.filter((t) => t.default === true);
|
|
38
|
+
if (defaults.length !== 1) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`api-targets.json: expected exactly one default target, found ${defaults.length}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return targets;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function loadTargetModules(
|
|
47
|
+
target: ApiTarget,
|
|
48
|
+
packageRoot: string = PACKAGE_ROOT,
|
|
49
|
+
): ModuleManifestEntry[] {
|
|
50
|
+
return target.modules.map((module) => {
|
|
51
|
+
const path = resolve(packageRoot, target.fixturesDir, module.fixture);
|
|
52
|
+
let raw: string;
|
|
53
|
+
try {
|
|
54
|
+
raw = readFileSync(path, "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`api-targets.json: target "${target.id}" module "${module.namespace}" fixture not found: ${path}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const entry: ModuleManifestEntry = {
|
|
61
|
+
namespace: module.namespace,
|
|
62
|
+
doc: JSON.parse(raw),
|
|
63
|
+
outFile: module.outFile,
|
|
64
|
+
importsFrom: target.coreTypesImport,
|
|
65
|
+
};
|
|
66
|
+
return module.skipFunctions ? { ...entry, skipFunctions: module.skipFunctions } : entry;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ModuleManifestEntry {
|
|
71
|
+
readonly namespace: string;
|
|
72
|
+
readonly doc: unknown;
|
|
73
|
+
readonly outFile: string;
|
|
74
|
+
readonly skipFunctions?: readonly string[];
|
|
75
|
+
readonly importsFrom?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ResolveTargetOptions {
|
|
79
|
+
readonly cacheDir?: string;
|
|
80
|
+
readonly download?: DownloadRefDoc;
|
|
81
|
+
readonly readZip?: typeof readZip;
|
|
82
|
+
readonly syncManifest?: readonly SyncManifestEntry[];
|
|
83
|
+
readonly packageRoot?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Source-aware module resolution: a `null`-source target reads committed
|
|
87
|
+
// fixtures from disk (delegates to loadTargetModules); a `ref-doc` target
|
|
88
|
+
// resolves its docs on demand from the version's ref-doc zip, keyed by the
|
|
89
|
+
// SYNC_MANIFEST namespace -> zip-entry map.
|
|
90
|
+
export async function resolveTargetModules(
|
|
91
|
+
target: ApiTarget,
|
|
92
|
+
opts: ResolveTargetOptions = {},
|
|
93
|
+
): Promise<ModuleManifestEntry[]> {
|
|
94
|
+
const source = target.source ?? null;
|
|
95
|
+
if (source == null) {
|
|
96
|
+
return loadTargetModules(target, opts.packageRoot);
|
|
97
|
+
}
|
|
98
|
+
const { zip } = await resolveRefDoc({
|
|
99
|
+
version: source.version,
|
|
100
|
+
cacheDir: opts.cacheDir ?? refDocCacheDir(),
|
|
101
|
+
...(opts.download ? { download: opts.download } : {}),
|
|
102
|
+
...(opts.readZip ? { readZip: opts.readZip } : {}),
|
|
103
|
+
});
|
|
104
|
+
const syncManifest = opts.syncManifest ?? SYNC_MANIFEST;
|
|
105
|
+
return target.modules.map((module) => {
|
|
106
|
+
const sync = syncManifest.find((s) => s.namespace === module.namespace);
|
|
107
|
+
if (!sync) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`api-targets.json: target "${target.id}" module "${module.namespace}": no SYNC_MANIFEST zip entry`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const entry: ModuleManifestEntry = {
|
|
113
|
+
namespace: module.namespace,
|
|
114
|
+
doc: JSON.parse(zip.read(sync.zipEntry)),
|
|
115
|
+
outFile: module.outFile,
|
|
116
|
+
importsFrom: target.coreTypesImport,
|
|
117
|
+
};
|
|
118
|
+
return module.skipFunctions ? { ...entry, skipFunctions: module.skipFunctions } : entry;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const API_TARGETS = loadApiTargets();
|
|
123
|
+
const DEFAULT_TARGET = API_TARGETS.find((t) => t.default === true) as ApiTarget;
|
|
124
|
+
|
|
125
|
+
export const MODULE_MANIFEST: readonly ModuleManifestEntry[] = loadTargetModules(DEFAULT_TARGET);
|
|
126
|
+
|
|
127
|
+
export interface MessagesManifestEntry {
|
|
128
|
+
readonly doc: unknown;
|
|
129
|
+
readonly outFile: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const MESSAGES_MANIFEST: MessagesManifestEntry = {
|
|
133
|
+
doc: messagesDoc,
|
|
134
|
+
outFile: "builtin-messages.d.ts",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export function generateBuiltinMessagesDeclaration(entry: MessagesManifestEntry): string {
|
|
138
|
+
return emitBuiltinMessages(parseMessagesDoc(entry.doc));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface GenerateResult {
|
|
142
|
+
contents: string;
|
|
143
|
+
dropped: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Every CONSTANT FQN across the manifest, so a module's emit can brand a
|
|
147
|
+
// constant token owned by a *different* module (e.g. render's
|
|
148
|
+
// `graphics.BUFFER_TYPE_*` params) to the same FQN-keyed brand the owning
|
|
149
|
+
// module's `const` declaration carries.
|
|
150
|
+
export function collectConstantFqns(
|
|
151
|
+
manifest: readonly ModuleManifestEntry[] = MODULE_MANIFEST,
|
|
152
|
+
): Set<string> {
|
|
153
|
+
const fqns = new Set<string>();
|
|
154
|
+
for (const entry of manifest) {
|
|
155
|
+
for (const c of parseDefoldApiDoc(entry.doc).constants) fqns.add(c.name);
|
|
156
|
+
}
|
|
157
|
+
return fqns;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface GenerateOptions {
|
|
161
|
+
knownConstantFqns?: ReadonlySet<string>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function generateModuleDeclaration(
|
|
165
|
+
entry: ModuleManifestEntry,
|
|
166
|
+
options?: GenerateOptions,
|
|
167
|
+
): GenerateResult {
|
|
168
|
+
const module = parseDefoldApiDoc(entry.doc);
|
|
169
|
+
const prefix = `${module.namespace}.`;
|
|
170
|
+
const dropped: string[] = [];
|
|
171
|
+
const skip = new Set(entry.skipFunctions ?? []);
|
|
172
|
+
module.functions = module.functions.filter((fn) => {
|
|
173
|
+
const local = fn.name.startsWith(prefix) ? fn.name.slice(prefix.length) : fn.name;
|
|
174
|
+
if (skip.has(local)) {
|
|
175
|
+
dropped.push(fn.name);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
const knownConstantFqns = options?.knownConstantFqns ?? collectConstantFqns();
|
|
181
|
+
const emitted = emitDeclarations(module, { knownConstantFqns });
|
|
182
|
+
const contents = wrapAsAmbientGlobal({
|
|
183
|
+
namespace: module.namespace,
|
|
184
|
+
emitted,
|
|
185
|
+
importsFrom: entry.importsFrom ?? "../src/core-types",
|
|
186
|
+
});
|
|
187
|
+
return { contents, dropped };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface VersionedModuleManifestEntry extends ModuleManifestEntry {
|
|
191
|
+
readonly versionId: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Committed generation covers only filesystem-fixture targets (source == null).
|
|
195
|
+
// ref-doc targets are resolved on the fly and never pre-baked, so they are
|
|
196
|
+
// excluded from the committed regen loop and the byte-drift guards.
|
|
197
|
+
export const VERSIONED_MODULE_MANIFEST: readonly VersionedModuleManifestEntry[] =
|
|
198
|
+
API_TARGETS.filter(
|
|
199
|
+
(target) => target.default !== true && (target.source ?? null) == null,
|
|
200
|
+
).flatMap((target) =>
|
|
201
|
+
loadTargetModules(target).map((entry) => ({ ...entry, versionId: target.id })),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
|
|
205
|
+
gui: "gui_script",
|
|
206
|
+
render: "render_script",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
|
|
210
|
+
"../builtin-messages",
|
|
211
|
+
"../../src/msg-overloads",
|
|
212
|
+
"../../src/go-overloads",
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
export interface KindManifestEntry {
|
|
216
|
+
readonly kind: string;
|
|
217
|
+
readonly restricted?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const KIND_MODULE_MANIFEST: readonly KindManifestEntry[] = [
|
|
221
|
+
{ kind: "script" },
|
|
222
|
+
{ kind: "gui-script", restricted: "gui" },
|
|
223
|
+
{ kind: "render-script", restricted: "render" },
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
export function generateKindIndex(kind: string): string {
|
|
227
|
+
const entry = KIND_MODULE_MANIFEST.find((e) => e.kind === kind);
|
|
228
|
+
if (!entry) throw new Error(`unknown script kind: ${kind}`);
|
|
229
|
+
const universalNamespaces = MODULE_MANIFEST.filter(
|
|
230
|
+
(m) => !Object.hasOwn(RESTRICTED_NAMESPACES, m.namespace),
|
|
231
|
+
).map((m) => `../${m.outFile.replace(/\.d\.ts$/, "")}`);
|
|
232
|
+
const lines = [...UNIVERSAL_EXTRA_IMPORTS, ...universalNamespaces]
|
|
233
|
+
.sort()
|
|
234
|
+
.map((path) => `import "${path}";`);
|
|
235
|
+
if (entry.restricted) lines.push(`import "../${entry.restricted}";`);
|
|
236
|
+
return `${lines.join("\n")}\n\nexport {};\n`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function generateVersionIndex(
|
|
240
|
+
versionId: string,
|
|
241
|
+
manifest: readonly VersionedModuleManifestEntry[] = VERSIONED_MODULE_MANIFEST,
|
|
242
|
+
): string {
|
|
243
|
+
const imports = manifest
|
|
244
|
+
.filter((entry) => entry.versionId === versionId)
|
|
245
|
+
.map((entry) => entry.outFile.replace(/\.d\.ts$/, ""))
|
|
246
|
+
.sort()
|
|
247
|
+
.map((module) => `import "./${module}";`)
|
|
248
|
+
.join("\n");
|
|
249
|
+
return `${imports}\n\nexport {};\n`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (import.meta.main) {
|
|
253
|
+
const generated = resolve(import.meta.dir, "..", "generated");
|
|
254
|
+
for (const entry of MODULE_MANIFEST) {
|
|
255
|
+
const { contents, dropped } = generateModuleDeclaration(entry);
|
|
256
|
+
if (dropped.length > 0) {
|
|
257
|
+
console.log(`note: dropped skipped member(s) from ${entry.namespace}: ${dropped.join(", ")}`);
|
|
258
|
+
}
|
|
259
|
+
const out = resolve(generated, entry.outFile);
|
|
260
|
+
writeFileSync(out, contents);
|
|
261
|
+
console.log(`wrote ${out}`);
|
|
262
|
+
}
|
|
263
|
+
const messagesOut = resolve(generated, MESSAGES_MANIFEST.outFile);
|
|
264
|
+
writeFileSync(messagesOut, generateBuiltinMessagesDeclaration(MESSAGES_MANIFEST));
|
|
265
|
+
console.log(`wrote ${messagesOut}`);
|
|
266
|
+
|
|
267
|
+
const versionIds = new Set(VERSIONED_MODULE_MANIFEST.map((entry) => entry.versionId));
|
|
268
|
+
for (const entry of VERSIONED_MODULE_MANIFEST) {
|
|
269
|
+
const { contents, dropped } = generateModuleDeclaration(entry);
|
|
270
|
+
if (dropped.length > 0) {
|
|
271
|
+
console.log(
|
|
272
|
+
`note: dropped skipped member(s) from ${entry.versionId}/${entry.namespace}: ${dropped.join(", ")}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const versionDir = resolve(generated, "versions", entry.versionId);
|
|
276
|
+
mkdirSync(versionDir, { recursive: true });
|
|
277
|
+
const out = resolve(versionDir, entry.outFile);
|
|
278
|
+
writeFileSync(out, contents);
|
|
279
|
+
console.log(`wrote ${out}`);
|
|
280
|
+
}
|
|
281
|
+
for (const versionId of versionIds) {
|
|
282
|
+
const indexOut = resolve(generated, "versions", versionId, "index.d.ts");
|
|
283
|
+
writeFileSync(indexOut, generateVersionIndex(versionId));
|
|
284
|
+
console.log(`wrote ${indexOut}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const kindsDir = resolve(generated, "kinds");
|
|
288
|
+
mkdirSync(kindsDir, { recursive: true });
|
|
289
|
+
for (const entry of KIND_MODULE_MANIFEST) {
|
|
290
|
+
const out = resolve(kindsDir, `${entry.kind}.d.ts`);
|
|
291
|
+
writeFileSync(out, generateKindIndex(entry.kind));
|
|
292
|
+
console.log(`wrote ${out}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { type ApiModule, parseDefoldApiDoc } from "../src/api-doc";
|
|
5
|
+
import { DEFOLD_TYPE_MAP } from "../src/core-types";
|
|
6
|
+
import { parseScriptApi } from "../src/script-api";
|
|
7
|
+
import { MODULE_MANIFEST } from "./regen";
|
|
8
|
+
|
|
9
|
+
export const DEFOLD_VERSION = "1.12.4";
|
|
10
|
+
export const refDocUrl = (version = DEFOLD_VERSION): string =>
|
|
11
|
+
`https://github.com/defold/defold/releases/download/${version}/ref-doc.zip`;
|
|
12
|
+
|
|
13
|
+
export interface SyncManifestEntry {
|
|
14
|
+
readonly namespace: string;
|
|
15
|
+
readonly zipEntry: string;
|
|
16
|
+
readonly fixture: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// namespace -> ref-doc.zip entry. Entry paths do not match the namespace (gui ->
|
|
20
|
+
// gui_script, go -> gameobject_script, most -> scripts-script_<n> / src-script_<n>),
|
|
21
|
+
// so each path is confirmed against the pinned release artifact, not derived.
|
|
22
|
+
export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
|
|
23
|
+
entry("b2d", "doc/scripts-box2d-script_box2d.cpp_doc.json"),
|
|
24
|
+
entry("buffer", "doc/scripts-script_buffer.cpp_doc.json"),
|
|
25
|
+
entry("camera", "doc/scripts-script_camera.cpp_doc.json"),
|
|
26
|
+
entry("collectionfactory", "doc/scripts-script_collection_factory.cpp_doc.json"),
|
|
27
|
+
entry("collectionproxy", "doc/scripts-script_collectionproxy.cpp_doc.json"),
|
|
28
|
+
entry("crash", "doc/script_crash.cpp_doc.json"),
|
|
29
|
+
entry("factory", "doc/scripts-script_factory.cpp_doc.json"),
|
|
30
|
+
entry("go", "doc/gameobject_script.cpp_doc.json"),
|
|
31
|
+
entry("graphics", "doc/src-script_graphics.cpp_doc.json"),
|
|
32
|
+
entry("gui", "doc/gui_script.cpp_doc.json"),
|
|
33
|
+
entry("http", "doc/scripts-script_http.cpp_doc.json"),
|
|
34
|
+
entry("image", "doc/scripts-script_image.cpp_doc.json"),
|
|
35
|
+
entry("json", "doc/src-script_json.cpp_doc.json"),
|
|
36
|
+
entry("label", "doc/scripts-script_label.cpp_doc.json"),
|
|
37
|
+
entry("liveupdate", "doc/src-script_liveupdate.h_doc.json"),
|
|
38
|
+
entry("model", "doc/scripts-script_model.cpp_doc.json"),
|
|
39
|
+
entry("msg", "doc/src-script_msg.cpp_doc.json"),
|
|
40
|
+
entry("particlefx", "doc/scripts-script_particlefx.cpp_doc.json"),
|
|
41
|
+
entry("physics", "doc/scripts-script_physics.cpp_doc.json"),
|
|
42
|
+
entry("profiler", "doc/profiler.cpp_doc.json"),
|
|
43
|
+
entry("render", "doc/render-render_script.cpp_doc.json"),
|
|
44
|
+
entry("resource", "doc/scripts-script_resource.cpp_doc.json"),
|
|
45
|
+
entry("socket", "doc/luasocket-luasocket.doc_h_doc.json"),
|
|
46
|
+
entry("sound", "doc/scripts-script_sound.cpp_doc.json"),
|
|
47
|
+
entry("sprite", "doc/scripts-script_sprite.cpp_doc.json"),
|
|
48
|
+
// The `sys` surface is split across two docs in ref-doc.zip; this is the core
|
|
49
|
+
// (src-script_sys) doc — the larger gamesys subset is folded in when wired.
|
|
50
|
+
entry("sys", "doc/src-script_sys.cpp_doc.json"),
|
|
51
|
+
entry("tilemap", "doc/scripts-script_tilemap.cpp_doc.json"),
|
|
52
|
+
entry("timer", "doc/src-script_timer.cpp_doc.json"),
|
|
53
|
+
entry("vmath", "doc/src-script_vmath.cpp_doc.json"),
|
|
54
|
+
entry("window", "doc/scripts-script_window.cpp_doc.json"),
|
|
55
|
+
entry("zlib", "doc/src-script_zlib.cpp_doc.json"),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Checklist namespaces with no machine-readable doc anywhere (neither core
|
|
59
|
+
// ref-doc.zip nor a published extension `.script_api`). Currently empty: the
|
|
60
|
+
// four extension-only surfaces are wired via EXTENSION_MANIFEST below.
|
|
61
|
+
export const UNMAPPED: ReadonlyMap<string, string> = new Map();
|
|
62
|
+
|
|
63
|
+
function entry(namespace: string, zipEntry: string): SyncManifestEntry {
|
|
64
|
+
return { namespace, zipEntry, fixture: `fixtures/${namespace}_doc.json` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ExtensionManifestEntry {
|
|
68
|
+
readonly namespace: string;
|
|
69
|
+
readonly repo: string;
|
|
70
|
+
readonly tag: string;
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly fixture: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extension-only namespaces: each ships a single `.script_api` (YAML) doc in
|
|
76
|
+
// its own repo rather than appearing in core ref-doc.zip. Pinned to a release
|
|
77
|
+
// tag and converted to the core ref-doc JSON shape via parseScriptApi so they
|
|
78
|
+
// flow through MODULE_MANIFEST and the drift guard like every core namespace.
|
|
79
|
+
export const EXTENSION_MANIFEST: readonly ExtensionManifestEntry[] = [
|
|
80
|
+
ext("iac", "defold/extension-iac", "1.4.0", "extension-iac/api/iac.script_api"),
|
|
81
|
+
ext("iap", "defold/extension-iap", "8.4.0", "extension-iap/api/iap.script_api"),
|
|
82
|
+
ext("push", "defold/extension-push", "4.1.0", "extension-push/api/push.script_api"),
|
|
83
|
+
ext("webview", "defold/extension-webview", "1.5.0", "webview/api/webview.script_api"),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function ext(namespace: string, repo: string, tag: string, path: string): ExtensionManifestEntry {
|
|
87
|
+
return { namespace, repo, tag, path, fixture: `fixtures/${namespace}_doc.json` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const extensionRawUrl = (e: ExtensionManifestEntry): string =>
|
|
91
|
+
`https://raw.githubusercontent.com/${e.repo}/${e.tag}/${e.path}`;
|
|
92
|
+
|
|
93
|
+
// Convert a raw `.script_api` YAML doc to the core ref-doc JSON string written
|
|
94
|
+
// to fixtures/<ns>_doc.json. The result is byte-compared by the same drift
|
|
95
|
+
// semantics core fixtures use.
|
|
96
|
+
export function scriptApiToFixtureJson(text: string): string {
|
|
97
|
+
return JSON.stringify(parseScriptApi(text));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The types-api-coverage `What` checklist in vision.md lists every namespace as
|
|
101
|
+
// a backtick-quoted token on the "Concrete breadth checklist" line. Reading it
|
|
102
|
+
// here keeps SYNC_MANIFEST + UNMAPPED honest against the source of truth: the
|
|
103
|
+
// coverage test fails if the checklist gains a namespace neither maps.
|
|
104
|
+
export function parseChecklistNamespaces(visionMarkdown: string): string[] {
|
|
105
|
+
const start = visionMarkdown.indexOf("Concrete breadth checklist");
|
|
106
|
+
if (start === -1) return [];
|
|
107
|
+
const lineEnd = visionMarkdown.indexOf("\n", start);
|
|
108
|
+
const line = visionMarkdown.slice(start, lineEnd === -1 ? undefined : lineEnd);
|
|
109
|
+
const afterParen = line.slice(line.indexOf("):") + 2);
|
|
110
|
+
const shipped = afterParen.indexOf("Currently shipped");
|
|
111
|
+
const segment = shipped === -1 ? afterParen : afterParen.slice(0, shipped);
|
|
112
|
+
const namespaces: string[] = [];
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
for (const match of segment.matchAll(/`([^`]+)`/g)) {
|
|
115
|
+
const token = match[1] as string;
|
|
116
|
+
if (!/^[a-z][a-z0-9]*$/.test(token) || seen.has(token)) continue;
|
|
117
|
+
seen.add(token);
|
|
118
|
+
namespaces.push(token);
|
|
119
|
+
}
|
|
120
|
+
return namespaces;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ZipAccessor {
|
|
124
|
+
has(entry: string): boolean;
|
|
125
|
+
read(entry: string): string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function readZip(path: string): ZipAccessor {
|
|
129
|
+
const list = Bun.spawnSync(["unzip", "-Z1", path]);
|
|
130
|
+
if (list.exitCode !== 0) {
|
|
131
|
+
throw new Error(`unzip -Z1 failed for ${path}: ${list.stderr.toString()}`);
|
|
132
|
+
}
|
|
133
|
+
const entries = new Set(list.stdout.toString().split("\n").filter(Boolean));
|
|
134
|
+
return {
|
|
135
|
+
has: (name) => entries.has(name),
|
|
136
|
+
read: (name) => {
|
|
137
|
+
const out = Bun.spawnSync(["unzip", "-p", path, name]);
|
|
138
|
+
if (out.exitCode !== 0) {
|
|
139
|
+
throw new Error(`unzip -p failed for ${name} in ${path}: ${out.stderr.toString()}`);
|
|
140
|
+
}
|
|
141
|
+
return out.stdout.toString();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function downloadZip(version = DEFOLD_VERSION): Promise<ZipAccessor> {
|
|
147
|
+
const url = refDocUrl(version);
|
|
148
|
+
const res = await fetch(url);
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
throw new Error(`download failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
151
|
+
}
|
|
152
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
153
|
+
const dir = mkdtempSync(join(tmpdir(), "defold-ref-doc-"));
|
|
154
|
+
const zipPath = join(dir, "ref-doc.zip");
|
|
155
|
+
writeFileSync(zipPath, bytes);
|
|
156
|
+
return readZip(zipPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ExtractedFixture {
|
|
160
|
+
namespace: string;
|
|
161
|
+
fixture: string;
|
|
162
|
+
contents: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function extractFixtures(
|
|
166
|
+
zip: ZipAccessor,
|
|
167
|
+
manifest: readonly SyncManifestEntry[] = SYNC_MANIFEST,
|
|
168
|
+
): ExtractedFixture[] {
|
|
169
|
+
return manifest.map((item) => {
|
|
170
|
+
if (!zip.has(item.zipEntry)) {
|
|
171
|
+
throw new Error(`zip is missing entry ${item.zipEntry} for namespace ${item.namespace}`);
|
|
172
|
+
}
|
|
173
|
+
return { namespace: item.namespace, fixture: item.fixture, contents: zip.read(item.zipEntry) };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type FixtureSyncStatus = "clean" | "drift" | "created";
|
|
178
|
+
|
|
179
|
+
export interface FixtureSyncResult {
|
|
180
|
+
namespace: string;
|
|
181
|
+
fixture: string;
|
|
182
|
+
status: FixtureSyncStatus;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SyncOptions {
|
|
186
|
+
fixturesRoot?: string;
|
|
187
|
+
check?: boolean;
|
|
188
|
+
manifest?: readonly SyncManifestEntry[];
|
|
189
|
+
format?: (raw: string) => string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const PACKAGE_ROOT = resolve(import.meta.dir, "..");
|
|
193
|
+
|
|
194
|
+
function canonicalJson(raw: string): string {
|
|
195
|
+
return JSON.stringify(JSON.parse(raw));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function biomeFormatJson(raw: string): string {
|
|
199
|
+
const out = Bun.spawnSync(["bunx", "biome", "format", "--stdin-file-path=fixture.json"], {
|
|
200
|
+
stdin: Buffer.from(raw),
|
|
201
|
+
});
|
|
202
|
+
if (out.exitCode !== 0) {
|
|
203
|
+
throw new Error(`biome format failed: ${out.stderr.toString()}`);
|
|
204
|
+
}
|
|
205
|
+
return out.stdout.toString();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function syncFixtures(zip: ZipAccessor, options: SyncOptions = {}): FixtureSyncResult[] {
|
|
209
|
+
const manifest = options.manifest ?? SYNC_MANIFEST;
|
|
210
|
+
return syncExtractedFixtures(extractFixtures(zip, manifest), options);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Compare/write a set of already-extracted fixtures against disk. Shared by
|
|
214
|
+
// the core (zip) and extension (`.script_api`) sync paths so both honor the
|
|
215
|
+
// identical created/clean/drift + format-on-write semantics.
|
|
216
|
+
export function syncExtractedFixtures(
|
|
217
|
+
items: readonly ExtractedFixture[],
|
|
218
|
+
options: Omit<SyncOptions, "manifest"> = {},
|
|
219
|
+
): FixtureSyncResult[] {
|
|
220
|
+
const root = options.fixturesRoot ?? PACKAGE_ROOT;
|
|
221
|
+
const check = options.check ?? false;
|
|
222
|
+
const format = options.format ?? biomeFormatJson;
|
|
223
|
+
const results: FixtureSyncResult[] = [];
|
|
224
|
+
for (const item of items) {
|
|
225
|
+
const path = resolve(root, item.fixture);
|
|
226
|
+
const existing = readFixtureOrNull(path);
|
|
227
|
+
const status: FixtureSyncStatus =
|
|
228
|
+
existing === null
|
|
229
|
+
? "created"
|
|
230
|
+
: canonicalJson(existing) === canonicalJson(item.contents)
|
|
231
|
+
? "clean"
|
|
232
|
+
: "drift";
|
|
233
|
+
if (!check && status !== "clean") {
|
|
234
|
+
writeFileSync(path, format(item.contents));
|
|
235
|
+
}
|
|
236
|
+
results.push({ namespace: item.namespace, fixture: item.fixture, status });
|
|
237
|
+
}
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function downloadExtensionFixtures(
|
|
242
|
+
manifest: readonly ExtensionManifestEntry[] = EXTENSION_MANIFEST,
|
|
243
|
+
): Promise<ExtractedFixture[]> {
|
|
244
|
+
const out: ExtractedFixture[] = [];
|
|
245
|
+
for (const e of manifest) {
|
|
246
|
+
const url = extensionRawUrl(e);
|
|
247
|
+
const res = await fetch(url);
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
throw new Error(`extension doc download failed: ${url} -> ${res.status} ${res.statusText}`);
|
|
250
|
+
}
|
|
251
|
+
out.push({
|
|
252
|
+
namespace: e.namespace,
|
|
253
|
+
fixture: e.fixture,
|
|
254
|
+
contents: scriptApiToFixtureJson(await res.text()),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readFixtureOrNull(path: string): string | null {
|
|
261
|
+
try {
|
|
262
|
+
return readFileSync(path, "utf8");
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface SyncedDoc {
|
|
269
|
+
namespace: string;
|
|
270
|
+
doc: unknown;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface CoverageReport {
|
|
274
|
+
wired: string[];
|
|
275
|
+
fixtureOnly: string[];
|
|
276
|
+
missingMapping: string[];
|
|
277
|
+
unknownTypeTokens: { namespace: string; tokens: string[] }[];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface CoverageInput {
|
|
281
|
+
manifest: readonly { namespace: string }[];
|
|
282
|
+
moduleManifest: readonly { namespace: string }[];
|
|
283
|
+
unmapped: ReadonlyMap<string, string>;
|
|
284
|
+
syncedDocs: readonly SyncedDoc[];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function buildCoverageReport(input: CoverageInput): CoverageReport {
|
|
288
|
+
const wiredSet = new Set(input.moduleManifest.map((e) => e.namespace));
|
|
289
|
+
const wired: string[] = [];
|
|
290
|
+
const fixtureOnly: string[] = [];
|
|
291
|
+
for (const item of input.manifest) {
|
|
292
|
+
(wiredSet.has(item.namespace) ? wired : fixtureOnly).push(item.namespace);
|
|
293
|
+
}
|
|
294
|
+
const missingMapping = [...input.unmapped.keys()];
|
|
295
|
+
|
|
296
|
+
const unknownTypeTokens: { namespace: string; tokens: string[] }[] = [];
|
|
297
|
+
for (const synced of input.syncedDocs) {
|
|
298
|
+
const module = parseDefoldApiDoc(synced.doc);
|
|
299
|
+
const tokens = collectUnknownTokens(module);
|
|
300
|
+
if (tokens.length > 0) {
|
|
301
|
+
unknownTypeTokens.push({ namespace: synced.namespace, tokens });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { wired, fixtureOnly, missingMapping, unknownTypeTokens };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function collectUnknownTokens(module: ApiModule): string[] {
|
|
308
|
+
const seen = new Set<string>();
|
|
309
|
+
const out: string[] = [];
|
|
310
|
+
const consider = (types: readonly string[]) => {
|
|
311
|
+
for (const token of types) {
|
|
312
|
+
if (Object.hasOwn(DEFOLD_TYPE_MAP, token) || seen.has(token)) continue;
|
|
313
|
+
seen.add(token);
|
|
314
|
+
out.push(token);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
for (const fn of module.functions) {
|
|
318
|
+
for (const param of fn.parameters) consider(param.types);
|
|
319
|
+
for (const ret of fn.returnValues) consider(ret.types);
|
|
320
|
+
}
|
|
321
|
+
for (const variable of module.variables) consider(variable.types);
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function printReport(results: FixtureSyncResult[], report: CoverageReport, check: boolean): void {
|
|
326
|
+
const verb = check ? "checked" : "synced";
|
|
327
|
+
console.log(`${verb} ${results.length} mapped fixture(s) from Defold ${DEFOLD_VERSION}`);
|
|
328
|
+
for (const status of ["created", "drift", "clean"] as const) {
|
|
329
|
+
const names = results.filter((r) => r.status === status).map((r) => r.namespace);
|
|
330
|
+
if (names.length > 0) console.log(` ${status}: ${names.join(", ")}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log("\ncoverage");
|
|
334
|
+
console.log(` wired (${report.wired.length}): ${report.wired.join(", ")}`);
|
|
335
|
+
console.log(` fixture-only (${report.fixtureOnly.length}): ${report.fixtureOnly.join(", ")}`);
|
|
336
|
+
console.log(
|
|
337
|
+
` missing-mapping (${report.missingMapping.length}): ${report.missingMapping.join(", ")}`,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (report.unknownTypeTokens.length > 0) {
|
|
341
|
+
console.log("\nunknown type tokens (would emit `unknown`)");
|
|
342
|
+
for (const { namespace, tokens } of report.unknownTypeTokens) {
|
|
343
|
+
console.log(` ${namespace}: ${tokens.join(", ")}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (import.meta.main) {
|
|
349
|
+
const args = process.argv.slice(2);
|
|
350
|
+
const check = args.includes("--check");
|
|
351
|
+
const zipPath = args.find((a) => !a.startsWith("--"));
|
|
352
|
+
const zip = zipPath ? readZip(zipPath) : await downloadZip();
|
|
353
|
+
|
|
354
|
+
const coreFixtures = extractFixtures(zip);
|
|
355
|
+
const extensionFixtures = await downloadExtensionFixtures();
|
|
356
|
+
const results = [
|
|
357
|
+
...syncExtractedFixtures(coreFixtures, { check }),
|
|
358
|
+
...syncExtractedFixtures(extensionFixtures, { check }),
|
|
359
|
+
];
|
|
360
|
+
const syncedDocs = [...coreFixtures, ...extensionFixtures].map((f) => ({
|
|
361
|
+
namespace: f.namespace,
|
|
362
|
+
doc: JSON.parse(f.contents),
|
|
363
|
+
}));
|
|
364
|
+
const report = buildCoverageReport({
|
|
365
|
+
manifest: [...SYNC_MANIFEST, ...EXTENSION_MANIFEST],
|
|
366
|
+
moduleManifest: MODULE_MANIFEST,
|
|
367
|
+
unmapped: UNMAPPED,
|
|
368
|
+
syncedDocs,
|
|
369
|
+
});
|
|
370
|
+
printReport(results, report, check);
|
|
371
|
+
|
|
372
|
+
if (check && results.some((r) => r.status === "drift")) {
|
|
373
|
+
process.exitCode = 1;
|
|
374
|
+
}
|
|
375
|
+
}
|