@defold-typescript/types 0.8.3 → 0.9.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/api-targets.json +18 -0
  3. package/generated/b2d_body.d.ts +348 -0
  4. package/generated/buffer.d.ts +3 -0
  5. package/generated/camera.d.ts +236 -1
  6. package/generated/collectionfactory.d.ts +4 -0
  7. package/generated/factory.d.ts +4 -0
  8. package/generated/font.d.ts +81 -0
  9. package/generated/go.d.ts +53 -0
  10. package/generated/gui.d.ts +264 -49
  11. package/generated/html5.d.ts +17 -13
  12. package/generated/http.d.ts +16 -0
  13. package/generated/image.d.ts +24 -0
  14. package/generated/json.d.ts +2 -0
  15. package/generated/kinds/gui-script.d.ts +3 -0
  16. package/generated/kinds/render-script.d.ts +3 -0
  17. package/generated/kinds/script.d.ts +3 -0
  18. package/generated/model.d.ts +14 -0
  19. package/generated/msg.d.ts +17 -11
  20. package/generated/particlefx.d.ts +6 -0
  21. package/generated/physics.d.ts +32 -0
  22. package/generated/profiler.d.ts +14 -0
  23. package/generated/render.d.ts +109 -0
  24. package/generated/resource.d.ts +223 -0
  25. package/generated/socket.d.ts +4 -0
  26. package/generated/sound.d.ts +6 -0
  27. package/generated/sprite.d.ts +5 -0
  28. package/generated/sys.d.ts +136 -0
  29. package/generated/tilemap.d.ts +2 -0
  30. package/generated/timer.d.ts +3 -0
  31. package/generated/vmath.d.ts +109 -93
  32. package/generated/window.d.ts +24 -1
  33. package/index.d.ts +8 -0
  34. package/package.json +1 -1
  35. package/scripts/fidelity-baseline.json +18 -2
  36. package/scripts/regen.ts +5 -0
  37. package/scripts/signature-store-io.ts +18 -0
  38. package/scripts/sync-api-docs.ts +208 -12
  39. package/src/core-types.ts +4 -2
  40. package/src/doc-comment.ts +42 -5
  41. package/src/emit-dts.ts +20 -1
  42. package/src/engine-globals.d.ts +2 -0
  43. package/src/example-store.ts +11 -7
  44. package/src/index.ts +18 -1
  45. package/src/msg-overloads.d.ts +3 -0
  46. package/src/signature-store.ts +20 -0
  47. package/src/window-event-guard.d.ts +52 -0
package/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import "./generated/builtin-messages";
4
4
  import "./src/msg-overloads";
5
5
  import "./src/message-guard";
6
+ import "./src/window-event-guard";
6
7
  import "./src/message-dispatch";
7
8
  import "./src/engine-globals";
8
9
  import "./generated/b2d";
@@ -56,7 +57,14 @@ export {
56
57
  type Vector3,
57
58
  type Vector4,
58
59
  } from "./src/core-types";
60
+ export { examplesHtmlToMarkdown, htmlToCodeText, htmlToDocText } from "./src/doc-comment";
59
61
  export { type EmitOptions, emitDeclarations } from "./src/emit-dts";
62
+ export {
63
+ hashExampleSource,
64
+ lookupTranslation,
65
+ type Translation,
66
+ type TranslationStore,
67
+ } from "./src/example-store";
60
68
  export {
61
69
  defineGuiScript,
62
70
  defineRenderScript,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,7 +7,7 @@
7
7
  "droppedMembers": 0,
8
8
  "optionalAsRequired": 0
9
9
  },
10
- "buffer": {
10
+ "b2d.body": {
11
11
  "droppedElements": 0,
12
12
  "unknownTokens": [],
13
13
  "recordTables": 0,
@@ -15,7 +15,7 @@
15
15
  "droppedMembers": 0,
16
16
  "optionalAsRequired": 0
17
17
  },
18
- "camera": {
18
+ "buffer": {
19
19
  "droppedElements": 0,
20
20
  "unknownTokens": [],
21
21
  "recordTables": 0,
@@ -23,6 +23,14 @@
23
23
  "droppedMembers": 0,
24
24
  "optionalAsRequired": 0
25
25
  },
26
+ "camera": {
27
+ "droppedElements": 0,
28
+ "unknownTokens": [],
29
+ "recordTables": 1,
30
+ "multiReturn": 0,
31
+ "droppedMembers": 0,
32
+ "optionalAsRequired": 7
33
+ },
26
34
  "collectionfactory": {
27
35
  "droppedElements": 0,
28
36
  "unknownTokens": [],
@@ -55,6 +63,14 @@
55
63
  "droppedMembers": 0,
56
64
  "optionalAsRequired": 0
57
65
  },
66
+ "font": {
67
+ "droppedElements": 0,
68
+ "unknownTokens": [],
69
+ "recordTables": 1,
70
+ "multiReturn": 0,
71
+ "droppedMembers": 0,
72
+ "optionalAsRequired": 0
73
+ },
58
74
  "go": {
59
75
  "droppedElements": 0,
60
76
  "unknownTokens": [],
package/scripts/regen.ts CHANGED
@@ -36,6 +36,10 @@ export interface ApiTarget {
36
36
  readonly coreTypesImport: string;
37
37
  readonly source?: ApiTargetSource;
38
38
  readonly modules: readonly ApiTargetModule[];
39
+ // Docs-only Lua stdlib pages (no generated `.d.ts`): vendored fixtures the
40
+ // docs-site pages under the "Lua standard library" category. Never read by
41
+ // regen/MODULE_MANIFEST; surfaced here so the registry stays type-honest.
42
+ readonly luaStdlib?: readonly { readonly namespace: string; readonly fixture: string }[];
39
43
  }
40
44
 
41
45
  const REGISTRY_PATH = resolve(import.meta.dir, "..", "api-targets.json");
@@ -234,6 +238,7 @@ const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
234
238
  "../../src/engine-globals",
235
239
  "../../src/msg-overloads",
236
240
  "../../src/message-guard",
241
+ "../../src/window-event-guard",
237
242
  "../../src/go-overloads",
238
243
  ];
239
244
 
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { SignatureStore } from "../src/signature-store";
4
+
5
+ // Build-time only: lives under `scripts/` (never reachable from the shipped
6
+ // `src/index.ts` graph) so its `node:fs` import cannot leak into a consumer's
7
+ // typecheck. `src/signature-store.ts` stays pure for exactly that reason.
8
+ const IO_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "io.json");
9
+
10
+ export function loadSignatures(path: string = IO_SIGNATURES_PATH): SignatureStore {
11
+ let raw: string;
12
+ try {
13
+ raw = readFileSync(path, "utf8");
14
+ } catch {
15
+ return {};
16
+ }
17
+ return JSON.parse(raw) as SignatureStore;
18
+ }
@@ -14,6 +14,11 @@ export interface SyncManifestEntry {
14
14
  readonly namespace: string;
15
15
  readonly zipEntry: string;
16
16
  readonly fixture: string;
17
+ // Extra ref-doc.zip entries whose elements are merged into this namespace at
18
+ // sync time. A namespace whose Lua surface is split across several upstream
19
+ // docs (e.g. `sys`) is reassembled before parse, so the emitter still sees one
20
+ // fixture, one namespace.
21
+ readonly mergeEntries?: readonly string[];
17
22
  }
18
23
 
19
24
  // namespace -> ref-doc.zip entry. Entry paths do not match the namespace (gui ->
@@ -21,12 +26,24 @@ export interface SyncManifestEntry {
21
26
  // so each path is confirmed against the pinned release artifact, not derived.
22
27
  export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
23
28
  entry("b2d", "doc/scripts-box2d-script_box2d.cpp_doc.json"),
29
+ // On-disk name uses underscores to avoid a double-dotted filename; the namespace
30
+ // string stays `b2d.body` so /api/b2d.body routes correctly and the emitter
31
+ // produces `declare global { namespace b2d.body { ... } }`. Use the non-`_defold`
32
+ // variant — the `_defold` variant has duplicate `get_world_center` elements.
33
+ entry(
34
+ "b2d.body",
35
+ "doc/scripts-box2d-script_box2d_body.cpp_doc.json",
36
+ "fixtures/b2d_body_doc.json",
37
+ ),
24
38
  entry("buffer", "doc/scripts-script_buffer.cpp_doc.json"),
25
- entry("camera", "doc/scripts-script_camera.cpp_doc.json"),
39
+ // camera's Lua surface lives in the render-script doc, not the scripts-script
40
+ // one (`doc/scripts-script_camera.cpp_doc.json` is empty upstream in 1.12.4).
41
+ entry("camera", "doc/render-render_script_camera.cpp_doc.json"),
26
42
  entry("collectionfactory", "doc/scripts-script_collection_factory.cpp_doc.json"),
27
43
  entry("collectionproxy", "doc/scripts-script_collectionproxy.cpp_doc.json"),
28
44
  entry("crash", "doc/script_crash.cpp_doc.json"),
29
45
  entry("factory", "doc/scripts-script_factory.cpp_doc.json"),
46
+ entry("font", "doc/scripts-script_font.cpp_doc.json"),
30
47
  entry("go", "doc/gameobject_script.cpp_doc.json"),
31
48
  entry("graphics", "doc/src-script_graphics.cpp_doc.json"),
32
49
  entry("gui", "doc/gui_script.cpp_doc.json"),
@@ -46,9 +63,16 @@ export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
46
63
  entry("socket", "doc/luasocket-luasocket.doc_h_doc.json"),
47
64
  entry("sound", "doc/scripts-script_sound.cpp_doc.json"),
48
65
  entry("sprite", "doc/scripts-script_sprite.cpp_doc.json"),
49
- // The `sys` surface is split across two docs in ref-doc.zip; this is the core
50
- // (src-script_sys) doc — the larger gamesys subset is folded in when wired.
51
- entry("sys", "doc/src-script_sys.cpp_doc.json"),
66
+ // The `sys` Lua surface is split across three docs in ref-doc.zip: the core
67
+ // (src-script_sys), the gamesys subset (`load_buffer`/`load_buffer_async` plus
68
+ // the `REQUEST_STATUS_*` constants), and the engine doc (`set_engine_throttle`/
69
+ // `set_render_enable`). `mergeEntries` folds all three into one fixture at sync
70
+ // time. The `*_ddf.proto` doc is deliberately excluded — it is message-shaped
71
+ // and owned by the builtin-messages catalog.
72
+ entry("sys", "doc/src-script_sys.cpp_doc.json", "fixtures/sys_doc.json", [
73
+ "doc/scripts-script_sys_gamesys.cpp_doc.json",
74
+ "doc/script-script_engine.cpp_doc.json",
75
+ ]),
52
76
  entry("tilemap", "doc/scripts-script_tilemap.cpp_doc.json"),
53
77
  entry("timer", "doc/src-script_timer.cpp_doc.json"),
54
78
  entry("types", "doc/src-script_types.cpp_doc.json"),
@@ -62,8 +86,61 @@ export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
62
86
  // four extension-only surfaces are wired via EXTENSION_MANIFEST below.
63
87
  export const UNMAPPED: ReadonlyMap<string, string> = new Map();
64
88
 
65
- function entry(namespace: string, zipEntry: string): SyncManifestEntry {
66
- return { namespace, zipEntry, fixture: `fixtures/${namespace}_doc.json` };
89
+ // A Lua script namespace is a lowercase, dot-separated identifier (`sys`,
90
+ // `b2d.body`). The native C/C++ SDK docs (`dmGraphics`, …), the C# bindings
91
+ // (`cs-*`), and the struct-only docs (empty `info.namespace`) fail this shape,
92
+ // so the upstream-coverage guard skips them structurally rather than listing
93
+ // each by hand — that keeps the guard self-maintaining as the SDK grows.
94
+ const LUA_NAMESPACE = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/;
95
+
96
+ // Lowercase-identifier namespaces that pass LUA_NAMESPACE but are intentionally
97
+ // not wired through SYNC_MANIFEST, each with a sourced reason (mirrors the
98
+ // `EMPTY_BY_UPSTREAM` allowlist shape). The upstream-coverage guard treats these
99
+ // as covered. Adding a namespace here is a deliberate, reviewed act.
100
+ export const IGNORED_UPSTREAM: ReadonlyMap<string, string> = new Map([
101
+ ["editor", "editor-scripting API (editor.apidoc), not a runtime game namespace"],
102
+ ["engine", "CLI/engine env doc, not a runtime Lua namespace"],
103
+ [
104
+ "builtins",
105
+ "global Defold builtins (hash/pprint/…) typed as ambient globals, not a `builtins.*` namespace",
106
+ ],
107
+ ]);
108
+
109
+ // Pure-Lua / LuaJIT surfaces Defold documents (https://defold.com/ref/stable/base-lua/,
110
+ // `bit-lua`) whose TypeScript types are owned by the `lua-types` dependency the
111
+ // `lua-stdlib-globals` goal adopted — `lua-types/special/jit-only.d.ts` declares
112
+ // `declare namespace bit { ... }` and `lua-types/core/global.d.ts` carries the
113
+ // base globals. Re-emitting them as generated Defold namespaces would collide
114
+ // (`declare namespace bit` duplicates; base globals are top-level globals, not
115
+ // a `base.*` namespace). LUA_STDLIB_MANIFEST vendors the same ref-doc JSON
116
+ // `SYNC_MANIFEST` carries, but the docs-site is the only consumer — `regen.ts`
117
+ // / `MODULE_MANIFEST` never read it, so no `generated/<ns>.d.ts` is produced.
118
+ export const LUA_STDLIB_MANIFEST: readonly SyncManifestEntry[] = [
119
+ entry("base", "doc/lua_base.doc_h_doc.json"),
120
+ entry("bit", "doc/src-script_bitop.cpp_doc.json"),
121
+ // Core Lua stdlib docs. The 1.12.4 release names these `doc/lua_<ns>.doc_h_doc.json`
122
+ // (confirmed against the pinned ref-doc.zip, not derived — the naming drifted from
123
+ // the 1.9.8 `doc/<ns>_doc.json` form). Typed by the lua-types dependency, so docs-only.
124
+ entry("math", "doc/lua_math.doc_h_doc.json"),
125
+ entry("os", "doc/lua_os.doc_h_doc.json"),
126
+ entry("string", "doc/lua_string.doc_h_doc.json"),
127
+ entry("table", "doc/lua_table.doc_h_doc.json"),
128
+ entry("coroutine", "doc/lua_coroutine.doc_h_doc.json"),
129
+ // Sandboxed-runtime stdlib surfaces. Defold sandboxes the Lua VM, but the pinned
130
+ // 1.12.4 ref-doc.zip documents all three with real function elements; types stay
131
+ // owned by lua-types (core/{debug,io}.d.ts + core/modules.d.ts for `package`).
132
+ entry("debug", "doc/lua_debug.doc_h_doc.json"),
133
+ entry("io", "doc/lua_io.doc_h_doc.json"),
134
+ entry("package", "doc/lua_package.doc_h_doc.json"),
135
+ ];
136
+
137
+ function entry(
138
+ namespace: string,
139
+ zipEntry: string,
140
+ fixture: string = `fixtures/${namespace}_doc.json`,
141
+ mergeEntries?: readonly string[],
142
+ ): SyncManifestEntry {
143
+ return { namespace, zipEntry, fixture, ...(mergeEntries ? { mergeEntries } : {}) };
67
144
  }
68
145
 
69
146
  export interface ExtensionManifestEntry {
@@ -115,7 +192,7 @@ export function parseChecklistNamespaces(visionMarkdown: string): string[] {
115
192
  const seen = new Set<string>();
116
193
  for (const match of segment.matchAll(/`([^`]+)`/g)) {
117
194
  const token = match[1] as string;
118
- if (!/^[a-z][a-z0-9]*$/.test(token) || seen.has(token)) continue;
195
+ if (!/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/.test(token) || seen.has(token)) continue;
119
196
  seen.add(token);
120
197
  namespaces.push(token);
121
198
  }
@@ -168,15 +245,52 @@ export interface ExtractedFixture {
168
245
  contents: string;
169
246
  }
170
247
 
248
+ function isRecord(value: unknown): value is Record<string, unknown> {
249
+ return typeof value === "object" && value !== null && !Array.isArray(value);
250
+ }
251
+
252
+ // Concatenate the `elements` of every doc into the first doc, deduping by
253
+ // (name, type) with first occurrence winning, and keep the first doc's `info`.
254
+ // First-wins guards the `b2d.body` `_defold` duplicate-`get_world_center`
255
+ // hazard. Operates on the raw ref-doc JSON shape (`{ info, elements }`), so the
256
+ // result is still parseable by `parseDefoldApiDoc`.
257
+ export function mergeApiDocs(docs: readonly unknown[]): unknown {
258
+ const first = docs[0];
259
+ const seen = new Set<string>();
260
+ const elements: unknown[] = [];
261
+ for (const doc of docs) {
262
+ if (!isRecord(doc) || !Array.isArray(doc.elements)) continue;
263
+ for (const element of doc.elements) {
264
+ const key =
265
+ isRecord(element) && typeof element.name === "string" && typeof element.type === "string"
266
+ ? `${element.type}${element.name}`
267
+ : null;
268
+ if (key !== null) {
269
+ if (seen.has(key)) continue;
270
+ seen.add(key);
271
+ }
272
+ elements.push(element);
273
+ }
274
+ }
275
+ return isRecord(first) ? { ...first, elements } : { elements };
276
+ }
277
+
171
278
  export function extractFixtures(
172
279
  zip: ZipAccessor,
173
280
  manifest: readonly SyncManifestEntry[] = SYNC_MANIFEST,
174
281
  ): ExtractedFixture[] {
175
282
  return manifest.map((item) => {
176
- if (!zip.has(item.zipEntry)) {
177
- throw new Error(`zip is missing entry ${item.zipEntry} for namespace ${item.namespace}`);
283
+ const sources = [item.zipEntry, ...(item.mergeEntries ?? [])];
284
+ for (const source of sources) {
285
+ if (!zip.has(source)) {
286
+ throw new Error(`zip is missing entry ${source} for namespace ${item.namespace}`);
287
+ }
178
288
  }
179
- return { namespace: item.namespace, fixture: item.fixture, contents: zip.read(item.zipEntry) };
289
+ const contents =
290
+ item.mergeEntries === undefined
291
+ ? zip.read(item.zipEntry)
292
+ : JSON.stringify(mergeApiDocs(sources.map((source) => JSON.parse(zip.read(source)))));
293
+ return { namespace: item.namespace, fixture: item.fixture, contents };
180
294
  });
181
295
  }
182
296
 
@@ -276,11 +390,26 @@ export interface SyncedDoc {
276
390
  doc: unknown;
277
391
  }
278
392
 
393
+ export interface UpstreamNamespace {
394
+ namespace: string;
395
+ functionCount: number;
396
+ zipEntry: string;
397
+ }
398
+
399
+ export interface UnmappedUpstreamNamespace {
400
+ namespace: string;
401
+ zipEntry: string;
402
+ }
403
+
279
404
  export interface CoverageReport {
280
405
  wired: string[];
281
406
  fixtureOnly: string[];
282
407
  missingMapping: string[];
283
408
  unknownTypeTokens: { namespace: string; tokens: string[] }[];
409
+ // Function-bearing Lua namespaces present in the zip but neither mapped nor
410
+ // allowlisted — the next `font`-style miss. The `--check` path fails when this
411
+ // is non-empty.
412
+ unmappedUpstream: UnmappedUpstreamNamespace[];
284
413
  }
285
414
 
286
415
  export interface CoverageInput {
@@ -288,6 +417,38 @@ export interface CoverageInput {
288
417
  moduleManifest: readonly { namespace: string }[];
289
418
  unmapped: ReadonlyMap<string, string>;
290
419
  syncedDocs: readonly SyncedDoc[];
420
+ // Every function-bearing namespace the zip itself exposes (from
421
+ // collectUpstreamNamespaces). Absent in pure-fixture callers that don't audit
422
+ // upstream coverage, in which case unmappedUpstream is empty.
423
+ upstream?: readonly UpstreamNamespace[];
424
+ upstreamMapped?: ReadonlySet<string>;
425
+ ignoredUpstream?: ReadonlyMap<string, string>;
426
+ }
427
+
428
+ // Enumerate every `doc/*.json` zip entry, reading its `info.namespace` and the
429
+ // count of FUNCTION elements. Only function-bearing docs are returned, so
430
+ // message-only / struct-only docs never reach the coverage guard.
431
+ export function collectUpstreamNamespaces(zip: ZipAccessor): UpstreamNamespace[] {
432
+ const out: UpstreamNamespace[] = [];
433
+ for (const zipEntry of zip.entries()) {
434
+ if (!/^doc\/.*\.json$/.test(zipEntry)) continue;
435
+ let doc: unknown;
436
+ try {
437
+ doc = JSON.parse(zip.read(zipEntry));
438
+ } catch {
439
+ continue;
440
+ }
441
+ if (!isRecord(doc)) continue;
442
+ const info = doc.info;
443
+ const namespace = isRecord(info) && typeof info.namespace === "string" ? info.namespace : "";
444
+ const elements = Array.isArray(doc.elements) ? doc.elements : [];
445
+ let functionCount = 0;
446
+ for (const element of elements) {
447
+ if (isRecord(element) && element.type === "FUNCTION") functionCount += 1;
448
+ }
449
+ if (functionCount > 0) out.push({ namespace, functionCount, zipEntry });
450
+ }
451
+ return out;
291
452
  }
292
453
 
293
454
  export function buildCoverageReport(input: CoverageInput): CoverageReport {
@@ -307,7 +468,26 @@ export function buildCoverageReport(input: CoverageInput): CoverageReport {
307
468
  unknownTypeTokens.push({ namespace: synced.namespace, tokens });
308
469
  }
309
470
  }
310
- return { wired, fixtureOnly, missingMapping, unknownTypeTokens };
471
+
472
+ const mapped = input.upstreamMapped ?? new Set<string>();
473
+ const ignored = input.ignoredUpstream ?? new Map<string, string>();
474
+ const flagged = new Map<string, string>();
475
+ for (const item of input.upstream ?? []) {
476
+ if (
477
+ !LUA_NAMESPACE.test(item.namespace) ||
478
+ mapped.has(item.namespace) ||
479
+ ignored.has(item.namespace) ||
480
+ flagged.has(item.namespace)
481
+ ) {
482
+ continue;
483
+ }
484
+ flagged.set(item.namespace, item.zipEntry);
485
+ }
486
+ const unmappedUpstream = [...flagged]
487
+ .map(([namespace, zipEntry]) => ({ namespace, zipEntry }))
488
+ .sort((a, b) => a.namespace.localeCompare(b.namespace));
489
+
490
+ return { wired, fixtureOnly, missingMapping, unknownTypeTokens, unmappedUpstream };
311
491
  }
312
492
 
313
493
  function collectUnknownTokens(module: ApiModule): string[] {
@@ -349,6 +529,15 @@ function printReport(results: FixtureSyncResult[], report: CoverageReport, check
349
529
  console.log(` ${namespace}: ${tokens.join(", ")}`);
350
530
  }
351
531
  }
532
+
533
+ if (report.unmappedUpstream.length > 0) {
534
+ console.log(
535
+ "\nunmapped upstream namespaces (function-bearing, neither mapped nor allowlisted)",
536
+ );
537
+ for (const { namespace, zipEntry } of report.unmappedUpstream) {
538
+ console.log(` ${namespace}: ${zipEntry}`);
539
+ }
540
+ }
352
541
  }
353
542
 
354
543
  if (import.meta.main) {
@@ -359,9 +548,11 @@ if (import.meta.main) {
359
548
 
360
549
  const coreFixtures = extractFixtures(zip);
361
550
  const extensionFixtures = await downloadExtensionFixtures();
551
+ const luaStdlibFixtures = extractFixtures(zip, LUA_STDLIB_MANIFEST);
362
552
  const results = [
363
553
  ...syncExtractedFixtures(coreFixtures, { check }),
364
554
  ...syncExtractedFixtures(extensionFixtures, { check }),
555
+ ...syncExtractedFixtures(luaStdlibFixtures, { check }),
365
556
  ];
366
557
  const syncedDocs = [...coreFixtures, ...extensionFixtures].map((f) => ({
367
558
  namespace: f.namespace,
@@ -372,10 +563,15 @@ if (import.meta.main) {
372
563
  moduleManifest: MODULE_MANIFEST,
373
564
  unmapped: UNMAPPED,
374
565
  syncedDocs,
566
+ upstream: collectUpstreamNamespaces(zip),
567
+ upstreamMapped: new Set(
568
+ [...SYNC_MANIFEST, ...EXTENSION_MANIFEST, ...LUA_STDLIB_MANIFEST].map((e) => e.namespace),
569
+ ),
570
+ ignoredUpstream: IGNORED_UPSTREAM,
375
571
  });
376
572
  printReport(results, report, check);
377
573
 
378
- if (check && results.some((r) => r.status === "drift")) {
574
+ if (check && (results.some((r) => r.status === "drift") || report.unmappedUpstream.length > 0)) {
379
575
  process.exitCode = 1;
380
576
  }
381
577
  }
package/src/core-types.ts CHANGED
@@ -17,7 +17,7 @@ export interface Vector3 {
17
17
  * @remarks
18
18
  * Prefer `v.unm()` over `-v` — TypeScript does not flag unary `-` on object
19
19
  * types and silently produces `number`. See
20
- * `docs/guide/typescript-gotchas.md` for the full story.
20
+ * `packages/docs/guide/typescript-gotchas.md` for the full story.
21
21
  */
22
22
  unm: LuaNegationMethod<Vector3>;
23
23
  }
@@ -35,7 +35,7 @@ export interface Vector4 {
35
35
  * @remarks
36
36
  * Prefer `v.unm()` over `-v` — TypeScript does not flag unary `-` on object
37
37
  * types and silently produces `number`. See
38
- * `docs/guide/typescript-gotchas.md` for the full story.
38
+ * `packages/docs/guide/typescript-gotchas.md` for the full story.
39
39
  */
40
40
  unm: LuaNegationMethod<Vector4>;
41
41
  }
@@ -128,6 +128,8 @@ export const DEFOLD_TYPE_MAP: Readonly<Record<string, string>> = {
128
128
  resource: 'Opaque<"resource">',
129
129
  b2World: 'Opaque<"b2World">',
130
130
  b2Body: 'Opaque<"b2Body">',
131
+ b2BodyType:
132
+ '(number & { readonly __brand: "b2d.body.B2_DYNAMIC_BODY" }) | (number & { readonly __brand: "b2d.body.B2_KINEMATIC_BODY" }) | (number & { readonly __brand: "b2d.body.B2_STATIC_BODY" })',
131
133
  client: 'Opaque<"client">',
132
134
  master: 'Opaque<"master">',
133
135
  unconnected: 'Opaque<"unconnected">',
@@ -29,18 +29,20 @@ export function htmlToDocText(html: string): string {
29
29
  .replace(/<li>/gi, "\n- ")
30
30
  .replace(/<\/li>/gi, "")
31
31
  .replace(/<br\s*\/?>/gi, "\n")
32
+ .replace(/<\/p>/gi, "\n\n")
32
33
  .replace(/<\/?pre>/gi, "\n")
33
34
  .replace(/<[^>]+>/g, "");
34
35
 
35
36
  text = decodeEntities(text);
36
37
 
37
- // Collapse horizontal whitespace runs, trim around newlines, drop blank
38
- // runs, then trim the whole string — preserving single newlines from lists
39
- // and `<br>`.
38
+ // Collapse horizontal whitespace runs, trim around newlines, fold runaway
39
+ // blank runs to one blank line, then trim the whole string — preserving
40
+ // single newlines from lists and `<br>`, and the paragraph-break blank line
41
+ // from `</p>`.
40
42
  text = text
41
43
  .replace(/[ \t]+/g, " ")
42
44
  .replace(/ *\n */g, "\n")
43
- .replace(/\n{2,}/g, "\n")
45
+ .replace(/\n{3,}/g, "\n\n")
44
46
  .trim();
45
47
 
46
48
  // A literal `*/` would close the JSDoc comment early; escape it.
@@ -68,6 +70,41 @@ export function htmlToCodeText(html: string): string {
68
70
  return collapsed.split("*/").join("*\\/");
69
71
  }
70
72
 
73
+ /**
74
+ * Convert a ref-doc `examples` HTML fragment — prose interleaved with one or
75
+ * more `<div class="codehilite">…</div>` syntax-highlight blocks — into Markdown:
76
+ * prose runs become `htmlToDocText`, each highlight block becomes a ` ```lua `
77
+ * fence via `htmlToCodeText`. A fragment with no `codehilite` block is wrapped
78
+ * whole as a single ` ```lua ` fence (back-compat for plain-code examples).
79
+ * Returns `""` for empty / whitespace-only input.
80
+ */
81
+ export function examplesHtmlToMarkdown(html: string): string {
82
+ if (html.trim() === "") return "";
83
+
84
+ const parts: string[] = [];
85
+ const blocks = /<div class="codehilite">([\s\S]*?)<\/div>/gi;
86
+ let lastIndex = 0;
87
+ let matched = false;
88
+ for (let match = blocks.exec(html); match !== null; match = blocks.exec(html)) {
89
+ matched = true;
90
+ const prose = htmlToDocText(html.slice(lastIndex, match.index));
91
+ if (prose !== "") parts.push(prose);
92
+ const code = htmlToCodeText(match[1] ?? "");
93
+ if (code !== "") parts.push(`\`\`\`lua\n${code}\n\`\`\``);
94
+ lastIndex = match.index + match[0].length;
95
+ }
96
+
97
+ if (!matched) {
98
+ const code = htmlToCodeText(html);
99
+ return code === "" ? "" : `\`\`\`lua\n${code}\n\`\`\``;
100
+ }
101
+
102
+ const trailing = htmlToDocText(html.slice(lastIndex));
103
+ if (trailing !== "") parts.push(trailing);
104
+
105
+ return parts.join("\n\n");
106
+ }
107
+
71
108
  export interface DocCommentParts {
72
109
  summary: string;
73
110
  params?: { name: string; doc: string }[];
@@ -92,7 +129,7 @@ export function renderDocComment(parts: DocCommentParts): string[] {
92
129
 
93
130
  const lines = ["/**"];
94
131
  for (const line of summaryLines) {
95
- lines.push(` * ${line}`);
132
+ lines.push(line === "" ? " *" : ` * ${line}`);
96
133
  }
97
134
 
98
135
  const hasTags = params.length > 0 || returns !== "" || example !== "";
package/src/emit-dts.ts CHANGED
@@ -499,6 +499,21 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
499
499
  ],
500
500
  ]);
501
501
 
502
+ // Slot-keyed (`element:param:name`, mirroring TABLE_SLOT_CURATIONS) replacements
503
+ // for a callback parameter's recovered signature, used where the generic
504
+ // `recoverCallbackSignature` `unknown`-everywhere form leaves real engine type
505
+ // information on the table. The value is the full emitted function type. Honest
506
+ // only: `window.set_listener` discriminates `event` by a known constant union and
507
+ // its `data` is a bare record (only resize carries fields — `isWindowEvent` is the
508
+ // path to typed `data`), mirroring the `msg.post` (send, typed) / `isMessage`
509
+ // (receive, narrow) split.
510
+ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map([
511
+ [
512
+ "window.set_listener:param:callback",
513
+ "(self: unknown, event: typeof WINDOW_EVENT_FOCUS_LOST | typeof WINDOW_EVENT_FOCUS_GAINED | typeof WINDOW_EVENT_RESIZED | typeof WINDOW_EVENT_ICONFIED | typeof WINDOW_EVENT_DEICONIFIED, data: Record<string | number, unknown>) => void",
514
+ ],
515
+ ]);
516
+
502
517
  // resource.set_atlas's `table` param and resource.get_atlas's `data` return are
503
518
  // flattened `<li><dl>` field lists whose `geometries` header is followed by the
504
519
  // `table`-typed siblings `vertices`/`uvs`/`indices` — the grouping heuristic
@@ -1200,7 +1215,11 @@ function mapSlotUnion(
1200
1215
  }
1201
1216
  }
1202
1217
  } else {
1203
- ts = mapType(token);
1218
+ const curated =
1219
+ slotKind !== undefined && slotName !== undefined
1220
+ ? CALLBACK_SIGNATURE_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
1221
+ : undefined;
1222
+ ts = curated ?? mapType(token);
1204
1223
  }
1205
1224
  if (seen.has(ts)) continue;
1206
1225
  seen.add(ts);
@@ -4,6 +4,8 @@ import type * as Core from "./core-types";
4
4
  declare global {
5
5
  type Hash = Core.Hash;
6
6
  function hash(s: string): Core.Hash;
7
+ function hash_to_hex(h: Core.Hash): string;
8
+ function pprint(v: unknown): void;
7
9
  type Opaque<Name extends string> = Core.Opaque<Name>;
8
10
  type Url = Core.Url;
9
11
  type Vector = Core.Vector;
@@ -7,7 +7,10 @@ export interface Translation {
7
7
  ts: string;
8
8
  }
9
9
 
10
- export type TranslationStore = Record<string, Translation>;
10
+ // An FQN maps to one translation per distinct example body it carries: an
11
+ // overloaded element (same name, differing `@example` source) contributes one
12
+ // array entry per body, each pinned by its own `sourceHash`.
13
+ export type TranslationStore = Record<string, Translation[]>;
11
14
 
12
15
  const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
13
16
  const FNV_PRIME = 0x100000001b3n;
@@ -30,15 +33,16 @@ export function hashExampleSource(source: string): string {
30
33
  return hash.toString(16).padStart(16, "0");
31
34
  }
32
35
 
33
- // Return the stored TypeScript body only when the FQN exists and its pinned
34
- // `sourceHash` matches the source we are about to emit; any mismatch returns
35
- // `null` so the caller keeps the Lua fallback.
36
+ // Return the stored TypeScript body only when the FQN exists and one of its
37
+ // pinned `sourceHash`es matches the source we are about to emit; any mismatch
38
+ // returns `null` so the caller keeps the Lua fallback.
36
39
  export function lookupTranslation(
37
40
  store: TranslationStore,
38
41
  fqn: string,
39
42
  sourceHash: string,
40
43
  ): string | null {
41
- const entry = store[fqn];
42
- if (!entry || entry.sourceHash !== sourceHash) return null;
43
- return entry.ts;
44
+ const entries = store[fqn];
45
+ if (!entries) return null;
46
+ const match = entries.find((entry) => entry.sourceHash === sourceHash);
47
+ return match ? match.ts : null;
44
48
  }
package/src/index.ts CHANGED
@@ -10,8 +10,20 @@ export {
10
10
  type Vector3,
11
11
  type Vector4,
12
12
  } from "./core-types";
13
- export { type DocCommentParts, htmlToDocText, renderDocComment } from "./doc-comment";
13
+ export {
14
+ type DocCommentParts,
15
+ examplesHtmlToMarkdown,
16
+ htmlToCodeText,
17
+ htmlToDocText,
18
+ renderDocComment,
19
+ } from "./doc-comment";
14
20
  export { type EmitOptions, emitDeclarations } from "./emit-dts";
21
+ export {
22
+ hashExampleSource,
23
+ lookupTranslation,
24
+ type Translation,
25
+ type TranslationStore,
26
+ } from "./example-store";
15
27
  export {
16
28
  defineGuiScript,
17
29
  defineRenderScript,
@@ -30,3 +42,8 @@ export {
30
42
  type ScriptProperty,
31
43
  } from "./lifecycle";
32
44
  export { type WrapOptions, wrapAsAmbientGlobal } from "./publish-dts";
45
+ export {
46
+ lookupSignature,
47
+ type SignatureOverride,
48
+ type SignatureStore,
49
+ } from "./signature-store";
@@ -12,8 +12,11 @@ declare global {
12
12
  * to a component. If the component part of the receiver is omitted, the message
13
13
  * is broadcast to all components in the game object.
14
14
  * The following receiver shorthands are available:
15
+ *
15
16
  * - `"."` the current game object
17
+ *
16
18
  * - `"#"` the current component
19
+ *
17
20
  * There is a 2 kilobyte limit to the message parameter table size.
18
21
  *
19
22
  * @param receiver - The receiver must be a string in URL-format, a URL object or a hashed string.