@defold-typescript/transpiler 0.30.0 → 0.32.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 (38) 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 +19 -9
  7. package/dist/index.js +1326 -396
  8. package/dist/library-dependencies.d.ts +47 -0
  9. package/dist/scene-address-declaration.d.ts +4 -2
  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 +3 -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 +52 -10
  26. package/src/library-dependencies.ts +337 -0
  27. package/src/scene-address-declaration.ts +29 -12
  28. package/src/scene-collection-roles.ts +267 -0
  29. package/src/scene-component-index.ts +74 -6
  30. package/src/scene-documents.ts +66 -6
  31. package/src/scene-naming-context.ts +130 -0
  32. package/src/scene-object-path-index.ts +172 -33
  33. package/src/transpile.ts +3 -0
  34. package/src/url-address-literals.ts +115 -0
  35. package/src/url-address-slots.ts +23 -2
  36. package/src/url-cross-world-address.ts +109 -0
  37. package/src/url-fragment-reachability.ts +72 -93
  38. package/src/sprite-animation-index.ts +0 -241
@@ -6,6 +6,9 @@
6
6
  import type { UrlParameterTable } from "@defold-typescript/types";
7
7
  import * as ts from "typescript";
8
8
  import type { SceneComponentIndex } from "./scene-component-index";
9
+ import { socketOfAddress } from "./scene-naming-context";
10
+ import type { SceneObjectPathIndex } from "./scene-object-path-index";
11
+ import { canonicalHashSymbols, isAmbient, staticAddressTextOf } from "./url-address-literals";
9
12
  import { addressClassOfArgument, isAddressClass } from "./url-address-slots";
10
13
 
11
14
  export interface UrlFragmentFinding {
@@ -16,103 +19,72 @@ export interface UrlFragmentFinding {
16
19
  readonly message: string;
17
20
  }
18
21
 
19
- export type UrlFragmentReport =
20
- | { readonly kind: "suppressed"; readonly reasons: readonly string[] }
21
- | { readonly kind: "checked"; readonly findings: readonly UrlFragmentFinding[] };
22
+ // The half of the joined index this check reads: which objects exist, what each
23
+ // owns, and — the precondition rather than decoration — whether the walk placed
24
+ // every world it found. `incomplete` is carried because a world it could not
25
+ // place contributes no key at all, so nothing downstream can notice its absence.
26
+ export type SceneObjectComponents = Pick<
27
+ SceneObjectPathIndex,
28
+ "paths" | "componentsOf" | "incomplete"
29
+ >;
22
30
 
23
- function isAmbient(fileName: string): boolean {
24
- return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
31
+ // The path portion of an index key, with any proxy socket stripped: `/enemy` for
32
+ // both the bootstrap `/enemy` and the proxied `mylevel:/enemy`.
33
+ function pathOfKey(key: string): string {
34
+ const socket = socketOfAddress(key);
35
+ return socket === undefined ? key : key.slice(socket.length + 1);
25
36
  }
26
37
 
27
- // The symbol a generic type reference was instantiated from — `undefined` for
28
- // anything that is not one.
29
- function referenceTargetSymbol(type: ts.Type): ts.Symbol | undefined {
30
- if ((type.flags & ts.TypeFlags.Object) === 0) return undefined;
31
- if (((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) === 0) return undefined;
32
- return (type as ts.TypeReference).target.symbol;
33
- }
34
-
35
- // What the global `Hash` *type* name means in scope at the anchor, as symbols.
36
- // The shipped `type Hash<S extends string = string> = Core.Hash<S>` yields the
37
- // `Core.Hash` symbol; a project's `Project.Hash` is not in global scope under
38
- // the bare name and never appears, and a project cannot add a second global
39
- // `Hash` type without a duplicate-identifier error — so unlike the *name*, this
40
- // anchor cannot be shadowed.
41
- function ambientHashAliasTargets(checker: ts.TypeChecker, anchor: ts.Node): ReadonlySet<ts.Symbol> {
42
- const targets = new Set<ts.Symbol>();
43
- for (const symbol of checker.getSymbolsInScope(anchor, ts.SymbolFlags.Type)) {
44
- if (symbol.name !== "Hash") continue;
45
- const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
46
- if (target) targets.add(target);
47
- }
48
- return targets;
49
- }
38
+ // The components the object an address names declares, or `undefined` wherever
39
+ // the answer is not provable — no object index, a fragment with no path, a
40
+ // relative path, a path no world declares, or a path some world declares but
41
+ // whose prototype could not be read. Every one of those falls back to the
42
+ // project-wide id set, because withholding a finding is the only safe direction.
43
+ function declaredComponentsOf(
44
+ objects: SceneObjectComponents | undefined,
45
+ path: string,
46
+ ): readonly string[] | undefined {
47
+ if (objects === undefined || path === "") return undefined;
50
48
 
51
- // The declarations *the* `Hash` can come from, resolved from the two anchors the
52
- // shipped ambient declarations carry one line apart rather than from the file
53
- // `Hash` happens to be declared in. A path test answers differently for an
54
- // installed `node_modules/@defold-typescript/types/…` and a workspace-resolved
55
- // `packages/types/…` copy of the very same declarations, which silently emptied
56
- // this report for anyone consuming the package through a `paths` mapping or a
57
- // symlink (bug-121); a symbol identity holds under both.
58
- //
59
- // The answer is the *intersection* of the two anchors. The `hash` function is
60
- // the scope-and-fail-closed anchor: its scope anchor is an ambient file so that
61
- // no user module's own `hash` participates, and every call signature is walked
62
- // because a project's global-script `function hash(s: string): string` *merges*
63
- // into this symbol rather than replacing it — the declarations then span both
64
- // files and a fully-qualified-name test would reject the real one. The global
65
- // `Hash` alias is the identity anchor: the function alone is not enough, because
66
- // a merged project overload contributes its own signatures, and testing what
67
- // they return by *name* admits any project type called `Hash` and then reports a
68
- // type argument that was never an address (bug-122). Asking the checker instead
69
- // of the filesystem is the same discipline as `tableKey` in
70
- // `url-address-slots.ts`.
71
- function canonicalHashSymbols(
72
- checker: ts.TypeChecker,
73
- program: ts.Program,
74
- ): ReadonlySet<ts.Symbol> {
75
- const files = program.getSourceFiles();
76
- const anchor = files.find((file) => isAmbient(file.fileName)) ?? files[0];
77
- const canonical = new Set<ts.Symbol>();
78
- if (!anchor) return canonical;
49
+ // A world `buildSceneCollectionRoles` could not place composes no key, so it
50
+ // is invisible to the withheld-`componentsOf` rule below: that rule defends a
51
+ // key that is in `paths`, and this one never got there. Every address form
52
+ // withdraws together, because `incomplete` carries free prose and there is no
53
+ // structured way to ask whether a given hole could own a given path. The
54
+ // project-wide test still runs — this is a fallback, never a suppression.
55
+ if (objects.incomplete.length > 0) return undefined;
79
56
 
80
- const aliased = ambientHashAliasTargets(checker, anchor);
57
+ if (socketOfAddress(path) !== undefined) {
58
+ return objects.paths.has(path) ? objects.componentsOf.get(path) : undefined;
59
+ }
60
+ if (!path.startsWith("/")) return undefined;
81
61
 
82
- for (const symbol of checker.getSymbolsInScope(anchor, ts.SymbolFlags.Function)) {
83
- if (symbol.name !== "hash") continue;
84
- const declaration = symbol.declarations?.[0];
85
- if (!declaration) continue;
86
- for (const signature of checker
87
- .getTypeOfSymbolAtLocation(symbol, declaration)
88
- .getCallSignatures()) {
89
- const target = referenceTargetSymbol(signature.getReturnType());
90
- if (target && aliased.has(target)) canonical.add(target);
91
- }
62
+ // A bare absolute path names one object per world, and the script writing it
63
+ // could be running in any of them, so the union is the only claim that holds
64
+ // whichever world it is. One withheld key withholds the whole union: a
65
+ // readable sibling must not stand in for a prototype nobody could read.
66
+ const declared = new Set<string>();
67
+ let known = false;
68
+ for (const key of objects.paths) {
69
+ if (pathOfKey(key) !== path) continue;
70
+ const owned = objects.componentsOf.get(key);
71
+ if (owned === undefined) return undefined;
72
+ known = true;
73
+ for (const id of owned) declared.add(id);
92
74
  }
93
- return canonical;
75
+ return known ? [...declared].sort() : undefined;
94
76
  }
95
77
 
96
- // The string a `Hash` was hashed from, recovered from its type argument —
97
- // `undefined` for anything else, including a bare `Hash`, a `Url`, a `string`,
98
- // and a union. `hash` records the argument in `Hash<S>` (see the `Hash` JSDoc in
99
- // `@defold-typescript/types`), so `const SPRITE = hash("#sprite")` carries the
100
- // text on the constant's type and no reassignment tracking is needed.
101
- //
102
- // The interface must be *the* `Hash`: a project's own generic type named `Hash`
103
- // resolves to its own declaration, and reading a type argument off it would
104
- // report a fragment that was never an address. `canonical` is what settles that.
105
- function hashedSourceOfType(
106
- checker: ts.TypeChecker,
107
- type: ts.Type,
108
- canonical: ReadonlySet<ts.Symbol>,
109
- ): string | undefined {
110
- const target = referenceTargetSymbol(type);
111
- if (!target || !canonical.has(target)) return undefined;
112
- const [source] = checker.getTypeArguments(type as ts.TypeReference);
113
- return source?.isStringLiteral() ? source.value : undefined;
78
+ function declaredPhrase(ids: readonly string[]): string {
79
+ return ids.length === 0
80
+ ? "it declares no components at all"
81
+ : `it declares ${ids.map((id) => `"${id}"`).join(", ")}`;
114
82
  }
115
83
 
84
+ export type UrlFragmentReport =
85
+ | { readonly kind: "suppressed"; readonly reasons: readonly string[] }
86
+ | { readonly kind: "checked"; readonly findings: readonly UrlFragmentFinding[] };
87
+
116
88
  // Report every address-slot expression whose text is statically known — a
117
89
  // quoted or backtick-quoted literal, or a value whose `Hash` type still carries
118
90
  // the string it was hashed from — whose `#fragment` names a component no
@@ -126,9 +98,10 @@ export function checkUrlFragmentReachability(input: {
126
98
  program: ts.Program;
127
99
  table: UrlParameterTable;
128
100
  index: SceneComponentIndex;
101
+ objects?: SceneObjectComponents;
129
102
  sourceFiles?: readonly ts.SourceFile[];
130
103
  }): UrlFragmentReport {
131
- const { program, table, index, sourceFiles } = input;
104
+ const { program, table, index, objects, sourceFiles } = input;
132
105
  if (index.incomplete.length > 0) {
133
106
  return { kind: "suppressed", reasons: index.incomplete };
134
107
  }
@@ -147,21 +120,27 @@ export function checkUrlFragmentReachability(input: {
147
120
 
148
121
  const visit = (node: ts.Node): void => {
149
122
  if (ts.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
150
- const text = ts.isStringLiteralLike(node)
151
- ? node.text
152
- : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
123
+ const text = staticAddressTextOf(checker, node, canonical);
153
124
  if (text !== undefined) {
154
125
  const hash = text.indexOf("#");
155
126
  const fragment = hash === -1 ? "" : text.slice(hash + 1);
156
- if (fragment !== "" && !index.ids.has(fragment)) {
127
+ const declared =
128
+ fragment === "" ? undefined : declaredComponentsOf(objects, text.slice(0, hash));
129
+ const reachable =
130
+ declared === undefined ? index.ids.has(fragment) : declared.includes(fragment);
131
+ if (fragment !== "" && !reachable) {
132
+ const path = text.slice(0, hash);
157
133
  findings.push({
158
134
  fileName: sourceFile.fileName,
159
135
  start: node.getStart(sourceFile),
160
136
  length: node.getWidth(sourceFile),
161
137
  fragment,
162
138
  message:
163
- `no \`.go\` or \`.collection\` in this project declares a component with the id ` +
164
- `"${fragment}", so this address cannot resolve at runtime`,
139
+ declared === undefined
140
+ ? `no \`.go\` or \`.collection\` in this project declares a component with the id ` +
141
+ `"${fragment}", so this address cannot resolve at runtime`
142
+ : `the game object "${path}" declares no component with the id "${fragment}" ` +
143
+ `(${declaredPhrase(declared)}), so this address cannot resolve at runtime`,
165
144
  });
166
145
  }
167
146
  }
@@ -1,241 +0,0 @@
1
- import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
2
-
3
- // The animation ids each script may play, keyed first by the project-relative
4
- // resource path of the `.script` that owns them and then by the id of the
5
- // sprite component they belong to, plus an honest record of every link that
6
- // could not be settled. A missing key means nothing may be offered for it; an
7
- // empty set means the component resolved and declares no animation.
8
- export interface SpriteAnimationIndex {
9
- readonly byScriptResource: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
10
- // The display path of the tile source each keyed component's ids were read
11
- // from, under the same two keys. Present for exactly the components
12
- // `byScriptResource` keys, so the two can never disagree about a resource.
13
- readonly tileSetByScriptResource: ReadonlyMap<string, ReadonlyMap<string, string>>;
14
- readonly unresolved: readonly string[];
15
- }
16
-
17
- // Resource paths inside a scene are project-absolute; the index is keyed the
18
- // way `buildGuiNodeIndex` keys its own, with the leading `/` dropped, so a
19
- // caller maps the file it holds forward through the build's output-path math
20
- // and looks the result up here.
21
- function resourceKey(resource: string): string {
22
- return resource.startsWith("/") ? resource.slice(1) : resource;
23
- }
24
-
25
- // The keys a caller supplies name files on the host, so they may arrive with
26
- // native separators; the resource paths they are matched against come out of
27
- // scene content and are always `/`-separated. Normalizing here enforces that
28
- // contract at the boundary instead of leaving it to whichever caller happens
29
- // to build the maps.
30
- function normalizeDocumentKey(path: string): string {
31
- return path.replaceAll("\\", "/");
32
- }
33
-
34
- const SCRIPT_SUFFIX = ".script";
35
- const SPRITE_SUFFIX = ".sprite";
36
-
37
- function firstField(message: SceneMessage, name: string): string | undefined {
38
- return (message.fields.get(name) ?? [])[0];
39
- }
40
-
41
- function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
42
- return message.messages.get(name) ?? [];
43
- }
44
-
45
- // Only `animations { id: … }` counts. Defold may also expose a bare
46
- // `images { image: … }` entry as a one-frame animation named after the file,
47
- // which is unverified here — and a wrong suggestion in this slot is a runtime
48
- // crash rather than a no-op.
49
- function declaredAnimations(document: SceneMessage): Set<string> {
50
- const ids = new Set<string>();
51
- for (const animation of childrenOf(document, "animations")) {
52
- for (const id of animation.fields.get("id") ?? []) {
53
- if (id !== "") ids.add(id);
54
- }
55
- }
56
- return ids;
57
- }
58
-
59
- interface AssetIndex {
60
- readonly animationsByTileSet: Map<string, Set<string>>;
61
- readonly tileSetBySprite: Map<string, string>;
62
- }
63
-
64
- function readAssets(assets: ReadonlyMap<string, string>, unresolved: string[]): AssetIndex {
65
- const animationsByTileSet = new Map<string, Set<string>>();
66
- const tileSetBySprite = new Map<string, string>();
67
-
68
- for (const [path, text] of assets) {
69
- const displayPath = normalizeDocumentKey(path);
70
- let document: SceneMessage;
71
- try {
72
- document = parseSceneTextFormat(text);
73
- } catch (error) {
74
- if (!(error instanceof SceneTextFormatError)) throw error;
75
- unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
76
- continue;
77
- }
78
- if (displayPath.endsWith(SPRITE_SUFFIX)) {
79
- const tileSet = firstField(document, "tile_set");
80
- if (tileSet !== undefined) tileSetBySprite.set(displayPath, resourceKey(tileSet));
81
- continue;
82
- }
83
- animationsByTileSet.set(displayPath, declaredAnimations(document));
84
- }
85
-
86
- return { animationsByTileSet, tileSetBySprite };
87
- }
88
-
89
- // The sprite components of one game object, as `id -> tile_set resource`. An
90
- // embedded sprite carries its own payload; a referenced one names a `.sprite`
91
- // document that carries it instead.
92
- function spriteTileSets(
93
- object: SceneMessage,
94
- displayPath: string,
95
- assets: AssetIndex,
96
- unresolved: string[],
97
- ): Map<string, string> {
98
- const tileSets = new Map<string, string>();
99
-
100
- for (const embedded of childrenOf(object, "embedded_components")) {
101
- if (firstField(embedded, "type") !== "sprite") continue;
102
- const id = firstField(embedded, "id");
103
- const payload = firstField(embedded, "data");
104
- if (id === undefined || payload === undefined) continue;
105
- try {
106
- const tileSet = firstField(parseSceneTextFormat(payload), "tile_set");
107
- if (tileSet !== undefined) tileSets.set(id, resourceKey(tileSet));
108
- } catch (error) {
109
- if (!(error instanceof SceneTextFormatError)) throw error;
110
- unresolved.push(
111
- `${displayPath}: the embedded sprite "${id}" could not be read (${error.message})`,
112
- );
113
- }
114
- }
115
-
116
- for (const referenced of childrenOf(object, "components")) {
117
- const component = firstField(referenced, "component");
118
- const id = firstField(referenced, "id");
119
- if (id === undefined || component === undefined || !component.endsWith(SPRITE_SUFFIX)) continue;
120
- const tileSet = assets.tileSetBySprite.get(resourceKey(component));
121
- if (tileSet === undefined) {
122
- unresolved.push(
123
- `${displayPath}: the sprite component "${id}" names ${component}, which is not among the project's sprite documents`,
124
- );
125
- continue;
126
- }
127
- tileSets.set(id, tileSet);
128
- }
129
-
130
- return tileSets;
131
- }
132
-
133
- function scriptResourcesOf(object: SceneMessage): string[] {
134
- const resources: string[] = [];
135
- for (const component of childrenOf(object, "components")) {
136
- const resource = firstField(component, "component");
137
- if (resource === undefined || !resource.endsWith(SCRIPT_SUFFIX)) continue;
138
- const key = resourceKey(resource);
139
- if (key !== SCRIPT_SUFFIX.slice(1)) resources.push(key);
140
- }
141
- return resources;
142
- }
143
-
144
- function isGameObject(message: SceneMessage): boolean {
145
- return message.messages.has("components") || message.messages.has("embedded_components");
146
- }
147
-
148
- export function buildSpriteAnimationIndex(input: {
149
- scenes: ReadonlyMap<string, string>;
150
- assets: ReadonlyMap<string, string>;
151
- }): SpriteAnimationIndex {
152
- const unresolved: string[] = [];
153
- const assets = readAssets(input.assets, unresolved);
154
- const byScriptResource = new Map<string, ReadonlyMap<string, ReadonlySet<string>>>();
155
- const tileSetByScriptResource = new Map<string, ReadonlyMap<string, string>>();
156
- const claimedBy = new Map<string, string>();
157
-
158
- // A `.go` document is a game object at its root; a `.collection` carries one
159
- // inside each `embedded_instances` `data:` payload, decoded exactly one level
160
- // at a time. No `instances { prototype: }` edge is followed: the referenced
161
- // `.go` is its own document and already claims its own script.
162
- function walk(message: SceneMessage, blockName: string, displayPath: string): void {
163
- if (isGameObject(message)) {
164
- claim(message, displayPath);
165
- }
166
- if (blockName === "embedded_instances") {
167
- for (const payload of message.fields.get("data") ?? []) {
168
- try {
169
- walk(parseSceneTextFormat(payload), "", displayPath);
170
- } catch (error) {
171
- if (!(error instanceof SceneTextFormatError)) throw error;
172
- unresolved.push(
173
- `${displayPath}: an embedded game object could not be read (${error.message})`,
174
- );
175
- }
176
- }
177
- }
178
- for (const [name, nested] of message.messages) {
179
- for (const child of nested) {
180
- walk(child, name, displayPath);
181
- }
182
- }
183
- }
184
-
185
- function claim(object: SceneMessage, displayPath: string): void {
186
- const scripts = scriptResourcesOf(object);
187
- if (scripts.length === 0) return;
188
-
189
- const tileSets = spriteTileSets(object, displayPath, assets, unresolved);
190
- const animations = new Map<string, ReadonlySet<string>>();
191
- const declaringTileSets = new Map<string, string>();
192
- for (const [id, tileSet] of tileSets) {
193
- const declared = assets.animationsByTileSet.get(tileSet);
194
- if (declared === undefined) {
195
- unresolved.push(
196
- `${displayPath}: the sprite component "${id}" names the tile source /${tileSet}, which is not among the project's asset documents`,
197
- );
198
- continue;
199
- }
200
- animations.set(id, declared);
201
- declaringTileSets.set(id, tileSet);
202
- }
203
-
204
- for (const key of scripts) {
205
- const owner = claimedBy.get(key);
206
- if (owner !== undefined) {
207
- byScriptResource.delete(key);
208
- tileSetByScriptResource.delete(key);
209
- unresolved.push(
210
- `${key}: claimed by both ${owner} and ${displayPath}, so its sprite animations are ambiguous`,
211
- );
212
- continue;
213
- }
214
- claimedBy.set(key, displayPath);
215
- byScriptResource.set(key, animations);
216
- tileSetByScriptResource.set(key, declaringTileSets);
217
- }
218
- }
219
-
220
- for (const [path, text] of input.scenes) {
221
- const displayPath = normalizeDocumentKey(path);
222
- try {
223
- walk(parseSceneTextFormat(text), "", displayPath);
224
- } catch (error) {
225
- if (!(error instanceof SceneTextFormatError)) throw error;
226
- unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
227
- }
228
- }
229
-
230
- return { byScriptResource, tileSetByScriptResource, unresolved };
231
- }
232
-
233
- // The component id a same-object `"#id"` address names, or `undefined` for
234
- // every other form. A path form names a component on a *different* game
235
- // object, so its id must never be resolved against this script's own.
236
- export function componentIdOfSameObjectAddress(address: string): string | undefined {
237
- if (!address.startsWith("#")) return undefined;
238
- const id = address.slice(1);
239
- if (id === "" || id.includes("/") || id.includes("#")) return undefined;
240
- return id;
241
- }