@defold-typescript/transpiler 0.29.0 → 0.31.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 (37) hide show
  1. package/dist/animation-sources.d.ts +6 -0
  2. package/dist/{sprite-animation-index.d.ts → component-animation-index.d.ts} +4 -4
  3. package/dist/config-key-index.d.ts +1 -0
  4. package/dist/gui-flipbook-index.d.ts +8 -0
  5. package/dist/gui-node-index.d.ts +21 -0
  6. package/dist/index.d.ts +21 -8
  7. package/dist/index.js +1317 -289
  8. package/dist/library-dependencies.d.ts +47 -0
  9. package/dist/scene-address-declaration.d.ts +14 -0
  10. package/dist/scene-collection-roles.d.ts +30 -0
  11. package/dist/scene-component-index.d.ts +20 -0
  12. package/dist/scene-documents.d.ts +21 -0
  13. package/dist/scene-naming-context.d.ts +66 -0
  14. package/dist/scene-object-path-index.d.ts +4 -1
  15. package/dist/url-address-literals.d.ts +10 -0
  16. package/dist/url-address-slots.d.ts +3 -1
  17. package/dist/url-cross-world-address.d.ts +41 -0
  18. package/dist/url-fragment-reachability.d.ts +3 -0
  19. package/package.json +2 -2
  20. package/src/animation-sources.ts +90 -0
  21. package/src/component-animation-index.ts +399 -0
  22. package/src/config-key-index.ts +28 -2
  23. package/src/gui-flipbook-index.ts +91 -0
  24. package/src/gui-node-index.ts +42 -4
  25. package/src/index.ts +66 -10
  26. package/src/library-dependencies.ts +337 -0
  27. package/src/scene-address-declaration.ts +74 -0
  28. package/src/scene-collection-roles.ts +267 -0
  29. package/src/scene-component-index.ts +74 -6
  30. package/src/scene-documents.ts +186 -0
  31. package/src/scene-naming-context.ts +130 -0
  32. package/src/scene-object-path-index.ts +172 -33
  33. package/src/url-address-literals.ts +115 -0
  34. package/src/url-address-slots.ts +23 -2
  35. package/src/url-cross-world-address.ts +109 -0
  36. package/src/url-fragment-reachability.ts +72 -93
  37. package/src/sprite-animation-index.ts +0 -241
package/src/index.ts CHANGED
@@ -1,7 +1,33 @@
1
- export { buildConfigKeyIndex } from "./config-key-index";
1
+ export type { ComponentAnimationIndex } from "./component-animation-index";
2
+ export {
3
+ buildComponentAnimationIndex,
4
+ componentIdOfSameObjectAddress,
5
+ } from "./component-animation-index";
6
+ export { buildConfigKeyIndex, readGameProjectSetting } from "./config-key-index";
7
+ export type { GuiFlipbookIndex } from "./gui-flipbook-index";
8
+ export { buildGuiFlipbookIndex } from "./gui-flipbook-index";
2
9
  export type { GuiNodeIndex } from "./gui-node-index";
3
- export { buildGuiNodeIndex } from "./gui-node-index";
10
+ export {
11
+ buildGuiNodeIndex,
12
+ guiScriptResourceOf,
13
+ guiScriptResourcesOf,
14
+ } from "./gui-node-index";
4
15
  export { buildInputActionIndex } from "./input-action-index";
16
+ export type {
17
+ ExtensionDependency,
18
+ LibraryIncludedEntries,
19
+ LibrarySceneDocument,
20
+ LibrarySceneDocuments,
21
+ LibrarySharedEntry,
22
+ } from "./library-dependencies";
23
+ export {
24
+ archiveWrapperOf,
25
+ isContainedResourcePath,
26
+ LIBRARY_DEPENDENCY_ROOT,
27
+ libraryIncludedEntries,
28
+ readGameProjectDependencies,
29
+ readLibrarySceneDocuments,
30
+ } from "./library-dependencies";
5
31
  export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
6
32
  export type { BuildConfig, ScriptKind, SourceOutputKind } from "./output-paths";
7
33
  export {
@@ -13,19 +39,42 @@ export {
13
39
  } from "./output-paths";
14
40
  export { getProgramDiagnostics } from "./program-diagnostics";
15
41
  export { isDefignoredPath, SCAFFOLDED_DEFIGNORE_LINES } from "./project-resources";
16
- export type { SceneComponentIndex } from "./scene-component-index";
17
- export { buildSceneComponentIndex } from "./scene-component-index";
42
+ export { buildSceneAddressDeclaration } from "./scene-address-declaration";
43
+ export type { SceneCollectionRoles, SceneCollectionRolesInput } from "./scene-collection-roles";
44
+ export { buildSceneCollectionRoles } from "./scene-collection-roles";
45
+ export type { ComponentDeclarations, SceneComponentIndex } from "./scene-component-index";
46
+ export {
47
+ buildSceneComponentIndex,
48
+ collectComponentDeclarations,
49
+ collectComponentIds,
50
+ } from "./scene-component-index";
51
+ export type { SceneReadHost } from "./scene-documents";
52
+ export {
53
+ ANIMATION_ASSET_EXTENSIONS,
54
+ COLLECTION_REFERENCE_EXTENSIONS,
55
+ displayPathOf,
56
+ GAME_PROJECT_DOCUMENT,
57
+ GUI_EXTENSIONS,
58
+ INPUT_BINDING_EXTENSIONS,
59
+ isExcludedProjectPath,
60
+ LIBRARY_SCENE_EXTENSIONS,
61
+ listProjectResourcePaths,
62
+ PROJECT_EXTENSIONS,
63
+ readSceneDocuments,
64
+ SCENE_EXTENSIONS,
65
+ } from "./scene-documents";
66
+ export type { NamingContext } from "./scene-naming-context";
67
+ export {
68
+ buildScriptNamingContexts,
69
+ relativeAddressesFrom,
70
+ socketOfAddress,
71
+ } from "./scene-naming-context";
18
72
  export type { SceneObjectPathIndex } from "./scene-object-path-index";
19
73
  export { buildSceneObjectPathIndex } from "./scene-object-path-index";
20
74
  export type { SceneMessage } from "./scene-text-format";
21
75
  export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
22
76
  export type { TranspileSession } from "./session";
23
77
  export { createTranspileSession } from "./session";
24
- export type { SpriteAnimationIndex } from "./sprite-animation-index";
25
- export {
26
- buildSpriteAnimationIndex,
27
- componentIdOfSameObjectAddress,
28
- } from "./sprite-animation-index";
29
78
  export type {
30
79
  TranspileDiagnostic,
31
80
  TranspileProjectInput,
@@ -36,9 +85,16 @@ export { transpile, transpileProject } from "./transpile";
36
85
  export type { ClassifiedSlot } from "./url-address-slots";
37
86
  export {
38
87
  addressClassOfArgument,
88
+ addressEntryOfArgument,
39
89
  isAddressClass,
40
90
  isFragmentCaret,
41
91
  resolveClassifiedSlotAtPosition,
42
92
  } from "./url-address-slots";
43
- export type { UrlFragmentFinding, UrlFragmentReport } from "./url-fragment-reachability";
93
+ export type { CrossWorldAddressFinding } from "./url-cross-world-address";
94
+ export { checkCrossWorldAddresses, slotRejectsForeignSocket } from "./url-cross-world-address";
95
+ export type {
96
+ SceneObjectComponents,
97
+ UrlFragmentFinding,
98
+ UrlFragmentReport,
99
+ } from "./url-fragment-reachability";
44
100
  export { checkUrlFragmentReachability } from "./url-fragment-reachability";
@@ -0,0 +1,337 @@
1
+ // What a Defold library dependency declares and what it actually shares. Both
2
+ // halves live here rather than beside the CLI resolver because the scene walk
3
+ // needs them too: it must read the declared set to tell an unresolved dependency
4
+ // from an absent one, and `packages/transpiler` cannot import `packages/cli`.
5
+ //
6
+ // `game.project` is read as the flat INI the editor writes, the same shape
7
+ // `buildConfigKeyIndex` walks — a key may carry a `#` (`dependencies#0`), which
8
+ // is part of its name and never a fragment.
9
+
10
+ import {
11
+ displayPathOf,
12
+ GAME_PROJECT_DOCUMENT,
13
+ LIBRARY_SCENE_EXTENSIONS,
14
+ type SceneReadHost,
15
+ } from "./scene-documents";
16
+
17
+ export interface ExtensionDependency {
18
+ readonly index: number;
19
+ readonly url: string;
20
+ }
21
+
22
+ function isProjectHeader(line: string): boolean {
23
+ return line.trim() === "[project]";
24
+ }
25
+
26
+ function isSectionHeader(line: string): boolean {
27
+ return /^\[.+\]\s*$/.test(line.trim());
28
+ }
29
+
30
+ export function readGameProjectDependencies(gameProjectText: string): ExtensionDependency[] {
31
+ const lines = gameProjectText.split("\n");
32
+ const deps: ExtensionDependency[] = [];
33
+ let inProject = false;
34
+ for (const line of lines) {
35
+ if (isSectionHeader(line)) {
36
+ inProject = isProjectHeader(line);
37
+ continue;
38
+ }
39
+ if (!inProject) {
40
+ continue;
41
+ }
42
+ const match = line.match(/^dependencies#(\d+)\s*=\s*(.+)$/);
43
+ if (match?.[1] !== undefined && match[2] !== undefined) {
44
+ deps.push({ index: Number(match[1]), url: match[2] });
45
+ }
46
+ }
47
+ return deps;
48
+ }
49
+
50
+ // The leading directory every entry sits under — GitHub packs an archive under
51
+ // `<repo>-<ref>/`, but a hand-rolled zip may not, and stripping a segment that
52
+ // is not shared would rebase a real directory away.
53
+ export function archiveWrapperOf(entries: readonly string[]): string | undefined {
54
+ let wrapper: string | undefined;
55
+ for (const entry of entries) {
56
+ const separator = entry.indexOf("/");
57
+ if (separator <= 0) {
58
+ return undefined;
59
+ }
60
+ const segment = entry.slice(0, separator);
61
+ if (wrapper === undefined) {
62
+ wrapper = segment;
63
+ } else if (wrapper !== segment) {
64
+ return undefined;
65
+ }
66
+ }
67
+ return wrapper;
68
+ }
69
+
70
+ function readLibraryIncludeDirs(gameProjectText: string): string[] {
71
+ let inLibrary = false;
72
+ for (const line of gameProjectText.split("\n")) {
73
+ if (isSectionHeader(line)) {
74
+ inLibrary = line.trim() === "[library]";
75
+ continue;
76
+ }
77
+ if (!inLibrary) {
78
+ continue;
79
+ }
80
+ const match = line.match(/^include_dirs\s*=\s*(.*)$/);
81
+ if (match?.[1] !== undefined) {
82
+ return match[1]
83
+ .split(",")
84
+ .map((dir) => dir.trim())
85
+ .filter((dir) => dir.length > 0);
86
+ }
87
+ }
88
+ return [];
89
+ }
90
+
91
+ export interface LibrarySharedEntry {
92
+ readonly entry: string;
93
+ readonly path: string;
94
+ }
95
+
96
+ export interface LibraryIncludedEntries {
97
+ readonly shared: LibrarySharedEntry[];
98
+ readonly reasons: string[];
99
+ // The entries that named an include dir and a scene kind but whose merged
100
+ // resource path would not stay inside it.
101
+ readonly refused: string[];
102
+ }
103
+
104
+ // The one containment rule both halves of the dependency surface read: the
105
+ // reader refuses to share a path this rejects, and the CLI writer re-asserts it
106
+ // before touching disk. A path only ever addresses something below its own root
107
+ // when it is relative, canonical, and uses the single separator Defold does —
108
+ // `path.join` normalizes `..` away rather than refusing it, and treats `\` as a
109
+ // separator on Windows only, so neither can be left to it.
110
+ export function isContainedResourcePath(path: string): boolean {
111
+ if (path.length === 0 || path.includes("\\")) {
112
+ return false;
113
+ }
114
+ return path
115
+ .split("/")
116
+ .every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
117
+ }
118
+
119
+ // The scene sources a library actually shares: the entries under the directories
120
+ // its own `game.project` names, at the resource path Defold itself would address
121
+ // them by (wrapper stripped, include dir kept — a library's `druid/druid.gui` is
122
+ // `/druid/druid.gui` in a project that depends on it).
123
+ export function libraryIncludedEntries(
124
+ entries: readonly string[],
125
+ gameProjectText: string | undefined,
126
+ ): LibraryIncludedEntries {
127
+ if (gameProjectText === undefined) {
128
+ return {
129
+ shared: [],
130
+ reasons: ["ships no game.project, so it declares no [library] include_dirs"],
131
+ refused: [],
132
+ };
133
+ }
134
+
135
+ const includeDirs = new Set(readLibraryIncludeDirs(gameProjectText));
136
+ if (includeDirs.size === 0) {
137
+ return {
138
+ shared: [],
139
+ reasons: ["its game.project declares no [library] include_dirs"],
140
+ refused: [],
141
+ };
142
+ }
143
+
144
+ const wrapper = archiveWrapperOf(entries);
145
+ const shared: LibrarySharedEntry[] = [];
146
+ const refused: string[] = [];
147
+ for (const entry of entries) {
148
+ const path = wrapper === undefined ? entry : entry.slice(wrapper.length + 1);
149
+ const [head] = path.split("/");
150
+ if (head === undefined || !includeDirs.has(head)) {
151
+ continue;
152
+ }
153
+ const dot = path.lastIndexOf(".");
154
+ if (dot === -1 || !LIBRARY_SCENE_EXTENSIONS.includes(path.slice(dot).toLowerCase())) {
155
+ continue;
156
+ }
157
+ // Applied last so `refused` names only what was otherwise going to be
158
+ // shared: `../evil.collection` never named an include dir to begin with.
159
+ if (!isContainedResourcePath(path)) {
160
+ refused.push(entry);
161
+ continue;
162
+ }
163
+ shared.push({ entry, path });
164
+ }
165
+ shared.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
166
+ return { shared, reasons: [], refused };
167
+ }
168
+
169
+ // The gitignored surface `resolve` unpacks each dependency's shared scene
170
+ // sources into, project-relative. Exported because the editor's index cache has
171
+ // to recognize an event under it: a literal there would drift from the path the
172
+ // walk composes.
173
+ export const LIBRARY_DEPENDENCY_ROOT = ".defold-types/dependencies";
174
+ const LIBRARY_DEPENDENCY_MANIFEST = `${LIBRARY_DEPENDENCY_ROOT}/dependencies.json`;
175
+
176
+ export interface LibrarySceneDocument {
177
+ // The merged Defold resource path this document is addressed by, which is the
178
+ // key the whole universe shares — see the composition note below.
179
+ readonly displayPath: string;
180
+ // Where the host holds it, so a watcher can be registered on the real file.
181
+ readonly hostPath: string;
182
+ readonly url: string;
183
+ readonly text: string;
184
+ }
185
+
186
+ export interface LibrarySceneDocuments {
187
+ readonly documents: LibrarySceneDocument[];
188
+ readonly unreadable: string[];
189
+ }
190
+
191
+ function joinProject(projectRoot: string, relative: string): string {
192
+ return `${projectRoot.replace(/\\/g, "/").replace(/\/+$/, "")}/${relative}`;
193
+ }
194
+
195
+ // `undefined` for anything this cannot be read as: a manifest that is not JSON
196
+ // and one whose shape says nothing about dependencies are the same hole to a
197
+ // caller, and both are repaired the same way.
198
+ function readMaterializedKeys(manifestText: string): Map<string, string> | undefined {
199
+ let parsed: unknown;
200
+ try {
201
+ parsed = JSON.parse(manifestText);
202
+ } catch {
203
+ return undefined;
204
+ }
205
+ const entries = (parsed as { dependencies?: unknown } | null)?.dependencies;
206
+ if (!Array.isArray(entries)) return undefined;
207
+
208
+ const keyByUrl = new Map<string, string>();
209
+ for (const entry of entries) {
210
+ const { key, url } = (entry ?? {}) as { key?: unknown; url?: unknown };
211
+ if (typeof key === "string" && typeof url === "string") keyByUrl.set(url, key);
212
+ }
213
+ return keyByUrl;
214
+ }
215
+
216
+ /**
217
+ * The scene sources the project's resolved library dependencies contribute,
218
+ * read out of the surface `resolve` materialized.
219
+ *
220
+ * Each document is keyed by its **merged** Defold resource path, not by one
221
+ * namespaced with its origin: Defold folds a library's `[library] include_dirs`
222
+ * into the consuming project's root namespace, and `scene-object-path-index.ts`
223
+ * resolves a `collection: "/druid/foo.collection"` reference by exactly that
224
+ * key — so an origin-namespaced key would strand every cross-document
225
+ * reference. The origin travels beside the document instead.
226
+ *
227
+ * Every refusal is one `unreadable` line naming the URL where one is known: an
228
+ * absent or unusable manifest, a declared dependency the last `resolve` did not
229
+ * materialize, a materialized directory `game.project` no longer declares, and
230
+ * a file that would not read. A dependency that contributes nothing silently is
231
+ * exactly the hole `readSceneDocuments` promises not to have.
232
+ */
233
+ export function readLibrarySceneDocuments(
234
+ host: SceneReadHost,
235
+ projectRoot: string,
236
+ extensions: readonly string[] = LIBRARY_SCENE_EXTENSIONS,
237
+ ): LibrarySceneDocuments {
238
+ const documents: LibrarySceneDocument[] = [];
239
+ const unreadable: string[] = [];
240
+
241
+ if (!host.readDirectory) return { documents, unreadable };
242
+
243
+ const gameProjectText = host.readFile(joinProject(projectRoot, GAME_PROJECT_DOCUMENT));
244
+ if (gameProjectText === undefined) return { documents, unreadable };
245
+
246
+ const declared = [...readGameProjectDependencies(gameProjectText)].sort(
247
+ (a, b) => a.index - b.index,
248
+ );
249
+ if (declared.length === 0) return { documents, unreadable };
250
+
251
+ const manifestText = host.readFile(joinProject(projectRoot, LIBRARY_DEPENDENCY_MANIFEST));
252
+ if (manifestText === undefined) {
253
+ for (const { url } of declared) {
254
+ unreadable.push(
255
+ `${url}: is declared by game.project but ${LIBRARY_DEPENDENCY_MANIFEST} is absent`,
256
+ );
257
+ }
258
+ return { documents, unreadable };
259
+ }
260
+
261
+ const keyByUrl = readMaterializedKeys(manifestText);
262
+ if (keyByUrl === undefined) {
263
+ for (const { url } of declared) {
264
+ unreadable.push(
265
+ `${url}: is declared by game.project but ${LIBRARY_DEPENDENCY_MANIFEST} is not a readable dependency manifest`,
266
+ );
267
+ }
268
+ return { documents, unreadable };
269
+ }
270
+
271
+ // Declared order decides a collision between two libraries, the way the
272
+ // `dependencies#N` order does in Defold itself — a host's directory order
273
+ // must not.
274
+ const resolved: { key: string; url: string }[] = [];
275
+ const urlByKey = new Map<string, string>();
276
+ for (const { url } of declared) {
277
+ const key = keyByUrl.get(url);
278
+ if (key === undefined) {
279
+ unreadable.push(
280
+ `${url}: is declared by game.project but was not materialized by the last resolve`,
281
+ );
282
+ continue;
283
+ }
284
+ resolved.push({ key, url });
285
+ urlByKey.set(key, url);
286
+ }
287
+
288
+ const root = joinProject(projectRoot, LIBRARY_DEPENDENCY_ROOT);
289
+ const filesByKey = new Map<string, string[]>();
290
+ const stray = new Set<string>();
291
+ for (const filePath of host.readDirectory(root, extensions)) {
292
+ const relative = displayPathOf(root, filePath);
293
+ // A host that answered for some directory other than the one it was handed
294
+ // would otherwise have its files keyed as though they sat under a
295
+ // dependency.
296
+ if (relative === filePath.replace(/\\/g, "/")) continue;
297
+ const separator = relative.indexOf("/");
298
+ if (separator <= 0) continue;
299
+ const key = relative.slice(0, separator);
300
+ const url = urlByKey.get(key);
301
+ if (url === undefined) {
302
+ stray.add(key);
303
+ continue;
304
+ }
305
+ const bucket = filesByKey.get(key);
306
+ if (bucket === undefined) filesByKey.set(key, [filePath]);
307
+ else bucket.push(filePath);
308
+ }
309
+
310
+ const claimed = new Map<string, string>();
311
+ for (const { key, url } of resolved) {
312
+ for (const hostPath of (filesByKey.get(key) ?? []).sort()) {
313
+ const displayPath = displayPathOf(root, hostPath).slice(key.length + 1);
314
+ if (displayPath === "") continue;
315
+ const owner = claimed.get(displayPath);
316
+ if (owner !== undefined) {
317
+ unreadable.push(`${displayPath}: also declared by ${url}; ${owner}'s file is used`);
318
+ continue;
319
+ }
320
+ const text = host.readFile(hostPath);
321
+ if (text === undefined) {
322
+ unreadable.push(`${displayPath}: could not be read from ${url}`);
323
+ continue;
324
+ }
325
+ claimed.set(displayPath, url);
326
+ documents.push({ displayPath, hostPath, url, text });
327
+ }
328
+ }
329
+
330
+ for (const key of [...stray].sort()) {
331
+ unreadable.push(
332
+ `${LIBRARY_DEPENDENCY_ROOT}/${key}: is materialized but no longer declared by game.project, so its scenes are left out`,
333
+ );
334
+ }
335
+
336
+ return { documents, unreadable };
337
+ }
@@ -0,0 +1,74 @@
1
+ import type { SceneCollectionRoles } from "./scene-collection-roles";
2
+ import { buildSceneComponentIndex } from "./scene-component-index";
3
+ import { buildSceneObjectPathIndex, type SceneObjectPathIndex } from "./scene-object-path-index";
4
+
5
+ const BANNER = `// Generated from this project's scenes by \`defold-typescript scene-types\`.
6
+ // Do not edit: every run rewrites it from the .go/.collection sources.
7
+ //
8
+ // Each key is an address the project declares. The aliases in
9
+ // \`@defold-typescript/types\` stay widened with \`(string & {})\`, so these keys
10
+ // only add completions — an address composed at runtime is never rejected.
11
+ `;
12
+
13
+ // Two forms, from two universes that *are* joined now. `buildSceneComponentIndex`
14
+ // reports bare component ids with no owning object, so the same-object form
15
+ // `#id` stays the whole address it can prove on its own — a `/path#id` key built
16
+ // from it alone would be a cross product claiming every object owns every
17
+ // component. The object-qualified form comes instead from the path walk's
18
+ // `componentsOf`, which attributes each id to the one object whose prototype
19
+ // declared it, and so carries the same world axis a path key does.
20
+ function componentAddressesOf(
21
+ documents: ReadonlyMap<string, string>,
22
+ paths: SceneObjectPathIndex,
23
+ ): string[] {
24
+ const keys = [...buildSceneComponentIndex(documents).ids].map((id) => `#${id}`);
25
+ for (const [path, ids] of paths.componentsOf) {
26
+ for (const id of ids) keys.push(`${path}#${id}`);
27
+ }
28
+ return keys;
29
+ }
30
+
31
+ // An interface with no members closes on the same line: an empty body written
32
+ // across two lines is what a formatter would collapse, and the generated file
33
+ // has to survive a consumer's formatter run byte-identical or every regeneration
34
+ // looks like a change.
35
+ function bodyOf(keys: readonly string[]): string {
36
+ if (keys.length === 0) return "{}";
37
+ const members = keys
38
+ .map((key) => ` ${JSON.stringify(key)}: true;\n`)
39
+ .sort()
40
+ .join("");
41
+ return `{\n${members} }`;
42
+ }
43
+
44
+ /**
45
+ * The global augmentation that fills `SceneGameObjectAddresses` and
46
+ * `SceneComponentAddresses` from already-read scene sources — keys are
47
+ * project-relative display paths, values are file text. `roles` says which
48
+ * world each collection is, so a path key carries the world it resolves in.
49
+ *
50
+ * Pure, like the two indexes it composes: the filesystem walk belongs to the
51
+ * caller. Keys are sorted so a project whose scenes did not change re-emits a
52
+ * byte-identical file, and a document map built in a different order emits the
53
+ * same bytes as well; a `Set` iteration order leaking into the output would
54
+ * make every regeneration rewrite the file and re-check the whole program.
55
+ */
56
+ export function buildSceneAddressDeclaration(
57
+ documents: ReadonlyMap<string, string>,
58
+ roles: SceneCollectionRoles,
59
+ ): string {
60
+ // One walk for both key sets: the qualified component addresses are the path
61
+ // universe joined to its own prototypes, not a second index over the same files.
62
+ const index = buildSceneObjectPathIndex(documents, roles);
63
+ const components = componentAddressesOf(documents, index);
64
+
65
+ return `${BANNER}
66
+ declare global {
67
+ interface SceneGameObjectAddresses ${bodyOf([...index.paths])}
68
+
69
+ interface SceneComponentAddresses ${bodyOf(components)}
70
+ }
71
+
72
+ export {};
73
+ `;
74
+ }