@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.
- package/dist/animation-sources.d.ts +6 -0
- package/dist/{sprite-animation-index.d.ts → component-animation-index.d.ts} +4 -4
- package/dist/config-key-index.d.ts +1 -0
- package/dist/gui-flipbook-index.d.ts +8 -0
- package/dist/gui-node-index.d.ts +21 -0
- package/dist/index.d.ts +21 -8
- package/dist/index.js +1317 -289
- package/dist/library-dependencies.d.ts +47 -0
- package/dist/scene-address-declaration.d.ts +14 -0
- package/dist/scene-collection-roles.d.ts +30 -0
- package/dist/scene-component-index.d.ts +20 -0
- package/dist/scene-documents.d.ts +21 -0
- package/dist/scene-naming-context.d.ts +66 -0
- package/dist/scene-object-path-index.d.ts +4 -1
- package/dist/url-address-literals.d.ts +10 -0
- package/dist/url-address-slots.d.ts +3 -1
- package/dist/url-cross-world-address.d.ts +41 -0
- package/dist/url-fragment-reachability.d.ts +3 -0
- package/package.json +2 -2
- package/src/animation-sources.ts +90 -0
- package/src/component-animation-index.ts +399 -0
- package/src/config-key-index.ts +28 -2
- package/src/gui-flipbook-index.ts +91 -0
- package/src/gui-node-index.ts +42 -4
- package/src/index.ts +66 -10
- package/src/library-dependencies.ts +337 -0
- package/src/scene-address-declaration.ts +74 -0
- package/src/scene-collection-roles.ts +267 -0
- package/src/scene-component-index.ts +74 -6
- package/src/scene-documents.ts +186 -0
- package/src/scene-naming-context.ts +130 -0
- package/src/scene-object-path-index.ts +172 -33
- package/src/url-address-literals.ts +115 -0
- package/src/url-address-slots.ts +23 -2
- package/src/url-cross-world-address.ts +109 -0
- package/src/url-fragment-reachability.ts +72 -93
- package/src/sprite-animation-index.ts +0 -241
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { readGameProjectSetting } from "./config-key-index";
|
|
2
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
3
|
+
|
|
4
|
+
// Which world each collection is, so an address can say where it resolves. A
|
|
5
|
+
// game object's address has two axes — which world, then which path inside it —
|
|
6
|
+
// and only these roles can tell them apart. Shaped like the indexes it feeds: a
|
|
7
|
+
// non-empty `incomplete` means the classification is not provably whole, so no
|
|
8
|
+
// consumer may conclude a collection has no role.
|
|
9
|
+
export interface SceneCollectionRoles {
|
|
10
|
+
/** The one collection `[bootstrap] main_collection` names, whose paths are bare. */
|
|
11
|
+
readonly bootstrap?: string | undefined;
|
|
12
|
+
/** Proxied collections, mapped to the socket `CollectionDesc.name` gives them. */
|
|
13
|
+
readonly sockets: ReadonlyMap<string, string>;
|
|
14
|
+
/** Collections another collection instances, whose paths exist only under that id. */
|
|
15
|
+
readonly instanced: ReadonlySet<string>;
|
|
16
|
+
/** Collection-factory prototypes, whose objects have no static path at all. */
|
|
17
|
+
readonly factoryPrototypes: ReadonlySet<string>;
|
|
18
|
+
readonly incomplete: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SceneCollectionRolesInput {
|
|
22
|
+
/** The `.go`/`.collection` universe, keyed by display path. */
|
|
23
|
+
readonly documents: ReadonlyMap<string, string>;
|
|
24
|
+
/** The `.collectionproxy`/`.collectionfactory` documents, keyed by display path. */
|
|
25
|
+
readonly references: ReadonlyMap<string, string>;
|
|
26
|
+
/** `game.project`'s text, or `undefined` when the walk could not read it. */
|
|
27
|
+
readonly gameProject: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The two places Defold embeds a whole escaped document in `data:`, the same
|
|
31
|
+
// rule `buildSceneComponentIndex` states — every other `data:` is a plain scalar.
|
|
32
|
+
const EMBEDDING_BLOCKS = new Set(["embedded_instances", "embedded_components"]);
|
|
33
|
+
|
|
34
|
+
// A component reference and the field its document holds the target under: a
|
|
35
|
+
// proxy names the collection it opens as a world, a factory names the prototype
|
|
36
|
+
// it spawns copies of.
|
|
37
|
+
const REFERENCE_KINDS = [
|
|
38
|
+
{ extension: ".collectionproxy", type: "collectionproxy", field: "collection" },
|
|
39
|
+
{ extension: ".collectionfactory", type: "collectionfactory", field: "prototype" },
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
type ReferenceKind = (typeof REFERENCE_KINDS)[number];
|
|
43
|
+
|
|
44
|
+
// Matches `scene-object-path-index.ts`: resource paths inside a scene are
|
|
45
|
+
// project-absolute, documents are keyed by project-relative display path.
|
|
46
|
+
function resourceKey(resource: string): string {
|
|
47
|
+
return resource.startsWith("/") ? resource.slice(1) : resource;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `game.project` and a `.collectionproxy` name the *compiled* resource the
|
|
51
|
+
// engine loads (`/main/main.collectionc`), not the source the walk read — the
|
|
52
|
+
// same normalisation `resolveBootPathScripts` applies to its own boot walk.
|
|
53
|
+
// Without it every real project fails to find its bootstrap world.
|
|
54
|
+
function collectionKey(resource: string): string {
|
|
55
|
+
return resourceKey(resource).replace(/\.collectionc$/, ".collection");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function firstField(message: SceneMessage, name: string): string | undefined {
|
|
59
|
+
return (message.fields.get(name) ?? [])[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Only a `.collection` is a world. A `.go` is a prototype, so running the role
|
|
63
|
+
// rules over it would make every prototype in the project a role-less
|
|
64
|
+
// "reachable no way" entry and drown the honest ones.
|
|
65
|
+
function isCollection(displayPath: string): boolean {
|
|
66
|
+
return displayPath.endsWith(".collection");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classify every collection the project declares into the world it is: the one
|
|
71
|
+
* bootstrap root, a proxy world addressed through its own socket, a collection
|
|
72
|
+
* instanced inside another, or a factory prototype with no static address.
|
|
73
|
+
*
|
|
74
|
+
* Pure, like the indexes it feeds — the filesystem walk belongs to the caller.
|
|
75
|
+
* There is deliberately no everything-else-is-a-root fallback: that fallback is
|
|
76
|
+
* what offers a bare `/enemy` for an object living in another world, and losing
|
|
77
|
+
* a completion is safe where offering a false one is not.
|
|
78
|
+
*/
|
|
79
|
+
export function buildSceneCollectionRoles(input: SceneCollectionRolesInput): SceneCollectionRoles {
|
|
80
|
+
const { documents, references, gameProject } = input;
|
|
81
|
+
const incomplete: string[] = [];
|
|
82
|
+
|
|
83
|
+
const parsed = new Map<string, SceneMessage>();
|
|
84
|
+
for (const [displayPath, text] of documents) {
|
|
85
|
+
try {
|
|
86
|
+
parsed.set(displayPath, parseSceneTextFormat(text));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
89
|
+
incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const instanced = new Set<string>();
|
|
94
|
+
for (const document of parsed.values()) {
|
|
95
|
+
for (const block of document.messages.get("collection_instances") ?? []) {
|
|
96
|
+
const collection = firstField(block, "collection");
|
|
97
|
+
if (collection !== undefined) instanced.add(resourceKey(collection));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const proxied = new Set<string>();
|
|
102
|
+
const factoryPrototypes = new Set<string>();
|
|
103
|
+
|
|
104
|
+
function record(kind: ReferenceKind, resource: string): void {
|
|
105
|
+
(kind.type === "collectionproxy" ? proxied : factoryPrototypes).add(collectionKey(resource));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A standalone reference is a component file of its own; its target lives in
|
|
109
|
+
// the reference document rather than in the scene that points at it, so a
|
|
110
|
+
// reference the walk never read is a named gap and not a dropped edge.
|
|
111
|
+
function followReferenceDocument(displayPath: string, resource: string): void {
|
|
112
|
+
const kind = REFERENCE_KINDS.find((candidate) => resource.endsWith(candidate.extension));
|
|
113
|
+
if (kind === undefined) return;
|
|
114
|
+
const text = references.get(resourceKey(resource));
|
|
115
|
+
if (text === undefined) {
|
|
116
|
+
incomplete.push(
|
|
117
|
+
`${displayPath}: the component ${resource} is not among the project's readable ${kind.extension} documents, so the collection it names is unclassified`,
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let document: SceneMessage;
|
|
122
|
+
try {
|
|
123
|
+
document = parseSceneTextFormat(text);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
126
|
+
incomplete.push(`${resourceKey(resource)}: could not be parsed (${error.message})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const target = firstField(document, kind.field);
|
|
130
|
+
if (target === undefined) {
|
|
131
|
+
incomplete.push(
|
|
132
|
+
`${resourceKey(resource)}: declares no ${kind.field}:, so the collection it names is unclassified`,
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
record(kind, target);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function walk(
|
|
140
|
+
message: SceneMessage,
|
|
141
|
+
blockName: string,
|
|
142
|
+
displayPath: string,
|
|
143
|
+
depth: number,
|
|
144
|
+
): void {
|
|
145
|
+
if (blockName === "components") {
|
|
146
|
+
const component = firstField(message, "component");
|
|
147
|
+
if (component !== undefined) followReferenceDocument(displayPath, component);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const payloads: SceneMessage[] = [];
|
|
151
|
+
if (EMBEDDING_BLOCKS.has(blockName)) {
|
|
152
|
+
for (const payload of message.fields.get("data") ?? []) {
|
|
153
|
+
try {
|
|
154
|
+
payloads.push(parseSceneTextFormat(payload));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
157
|
+
incomplete.push(
|
|
158
|
+
`${displayPath}: embedded payload at depth ${depth + 1} could not be parsed (${error.message})`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (blockName === "embedded_components") {
|
|
165
|
+
const type = firstField(message, "type");
|
|
166
|
+
const kind = REFERENCE_KINDS.find((candidate) => candidate.type === type);
|
|
167
|
+
if (kind !== undefined) {
|
|
168
|
+
for (const payload of payloads) {
|
|
169
|
+
const target = firstField(payload, kind.field);
|
|
170
|
+
if (target === undefined) {
|
|
171
|
+
incomplete.push(
|
|
172
|
+
`${displayPath}: the embedded ${kind.type} declares no ${kind.field}:, so the collection it names is unclassified`,
|
|
173
|
+
);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
record(kind, target);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const payload of payloads) walk(payload, "", displayPath, depth + 1);
|
|
182
|
+
for (const [name, nested] of message.messages) {
|
|
183
|
+
for (const child of nested) walk(child, name, displayPath, depth);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const [displayPath, document] of parsed) walk(document, "", displayPath, 0);
|
|
188
|
+
|
|
189
|
+
// The socket is the proxied collection's own `name:` — `CollectionDesc.name`,
|
|
190
|
+
// which the editor writes on the collection itself and which is deliberately
|
|
191
|
+
// neither the proxy component's id nor the file's basename.
|
|
192
|
+
const sockets = new Map<string, string>();
|
|
193
|
+
const byName = new Map<string, string[]>();
|
|
194
|
+
for (const displayPath of [...proxied].sort()) {
|
|
195
|
+
const document = parsed.get(displayPath);
|
|
196
|
+
if (document === undefined) {
|
|
197
|
+
incomplete.push(
|
|
198
|
+
`${displayPath}: is opened as a collection proxy but is not among the project's readable scene documents, so its world has no name`,
|
|
199
|
+
);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const name = firstField(document, "name");
|
|
203
|
+
if (name === undefined || name === "") {
|
|
204
|
+
incomplete.push(
|
|
205
|
+
`${displayPath}: is opened as a collection proxy but declares no name:, so its world cannot be addressed`,
|
|
206
|
+
);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
sockets.set(displayPath, name);
|
|
210
|
+
byName.set(name, [...(byName.get(name) ?? []), displayPath]);
|
|
211
|
+
}
|
|
212
|
+
for (const [name, holders] of byName) {
|
|
213
|
+
if (holders.length > 1) {
|
|
214
|
+
incomplete.push(
|
|
215
|
+
`${holders.join(", ")}: all declare name: "${name}", so an address under that socket does not say which world it means`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A project the walk found no collection in has no world to name, so an
|
|
221
|
+
// absent `[bootstrap]` says nothing a reader could act on — the same posture
|
|
222
|
+
// `sceneIndexForBuild` takes towards a scene-less project. A project that does
|
|
223
|
+
// have collections is a different case: there the absence is exactly why no
|
|
224
|
+
// address is offered bare, so it is always named.
|
|
225
|
+
const hasCollections = [...parsed.keys()].some(isCollection);
|
|
226
|
+
|
|
227
|
+
let bootstrap: string | undefined;
|
|
228
|
+
if (gameProject === undefined) {
|
|
229
|
+
if (hasCollections) {
|
|
230
|
+
incomplete.push(
|
|
231
|
+
"game.project could not be read, so no collection is known to be the bootstrap world and no address is offered unqualified",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
const main = readGameProjectSetting(gameProject, "bootstrap", "main_collection");
|
|
236
|
+
if (main === undefined) {
|
|
237
|
+
if (hasCollections) {
|
|
238
|
+
incomplete.push(
|
|
239
|
+
"game.project declares no [bootstrap] main_collection, so no collection is the bootstrap world",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
} else if (!parsed.has(collectionKey(main))) {
|
|
243
|
+
incomplete.push(
|
|
244
|
+
`game.project names ${main} as [bootstrap] main_collection, which is not among the project's readable scene documents`,
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
bootstrap = collectionKey(main);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const displayPath of parsed.keys()) {
|
|
252
|
+
if (!isCollection(displayPath)) continue;
|
|
253
|
+
if (
|
|
254
|
+
displayPath === bootstrap ||
|
|
255
|
+
sockets.has(displayPath) ||
|
|
256
|
+
instanced.has(displayPath) ||
|
|
257
|
+
factoryPrototypes.has(displayPath)
|
|
258
|
+
) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
incomplete.push(
|
|
262
|
+
`${displayPath}: is reached by no bootstrap, proxy, instance or factory reference, so which world its objects live in is unknown`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { bootstrap, sockets, instanced, factoryPrototypes, incomplete };
|
|
267
|
+
}
|
|
@@ -20,23 +20,56 @@ const COMPONENT_BLOCKS = new Set(["components", "embedded_components"]);
|
|
|
20
20
|
// projects as unreadable and suppress the check for good.
|
|
21
21
|
const EMBEDDING_BLOCKS = new Set(["embedded_instances", "embedded_components"]);
|
|
22
22
|
|
|
23
|
+
// A component id and the resource it references, where one is referenced at all.
|
|
24
|
+
// The two axes are gathered by one pass because they are read off the same
|
|
25
|
+
// block: a second walk over the same documents is exactly how the id universe
|
|
26
|
+
// and the resource universe drift apart.
|
|
27
|
+
export interface ComponentDeclarations {
|
|
28
|
+
readonly ids: Set<string>;
|
|
29
|
+
readonly resources: Map<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Resource paths inside a scene are project-absolute; documents are keyed by
|
|
33
|
+
// project-relative display path, the same convention `resourceKey` follows in
|
|
34
|
+
// `scene-object-path-index.ts`.
|
|
35
|
+
function resourceKey(resource: string): string {
|
|
36
|
+
return resource.startsWith("/") ? resource.slice(1) : resource;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
function collect(
|
|
24
40
|
message: SceneMessage,
|
|
25
41
|
blockName: string,
|
|
26
42
|
depth: number,
|
|
27
43
|
displayPath: string,
|
|
28
|
-
|
|
44
|
+
declarations: ComponentDeclarations,
|
|
29
45
|
incomplete: string[],
|
|
30
46
|
): void {
|
|
31
47
|
if (COMPONENT_BLOCKS.has(blockName)) {
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
const declared = message.fields.get("id") ?? [];
|
|
49
|
+
for (const id of declared) {
|
|
50
|
+
declarations.ids.add(id);
|
|
51
|
+
}
|
|
52
|
+
// `embedded_components` is skipped on purpose: its payload *is* the
|
|
53
|
+
// component, so it names no resource and must never be credited with one.
|
|
54
|
+
if (blockName === "components") {
|
|
55
|
+
const [id] = declared;
|
|
56
|
+
const [component] = message.fields.get("component") ?? [];
|
|
57
|
+
if (id !== undefined && id !== "" && component !== undefined) {
|
|
58
|
+
declarations.resources.set(id, resourceKey(component));
|
|
59
|
+
}
|
|
34
60
|
}
|
|
35
61
|
}
|
|
36
62
|
if (EMBEDDING_BLOCKS.has(blockName)) {
|
|
37
63
|
for (const payload of message.fields.get("data") ?? []) {
|
|
38
64
|
try {
|
|
39
|
-
collect(
|
|
65
|
+
collect(
|
|
66
|
+
parseSceneTextFormat(payload),
|
|
67
|
+
"",
|
|
68
|
+
depth + 1,
|
|
69
|
+
displayPath,
|
|
70
|
+
declarations,
|
|
71
|
+
incomplete,
|
|
72
|
+
);
|
|
40
73
|
} catch (error) {
|
|
41
74
|
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
42
75
|
incomplete.push(
|
|
@@ -47,11 +80,44 @@ function collect(
|
|
|
47
80
|
}
|
|
48
81
|
for (const [name, nested] of message.messages) {
|
|
49
82
|
for (const child of nested) {
|
|
50
|
-
collect(child, name, depth, displayPath,
|
|
83
|
+
collect(child, name, depth, displayPath, declarations, incomplete);
|
|
51
84
|
}
|
|
52
85
|
}
|
|
53
86
|
}
|
|
54
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Every component id one already-parsed scene message declares, plus the
|
|
90
|
+
* resource each *referenced* component names, following the two `data:`
|
|
91
|
+
* payloads Defold embeds a whole document in. Exported because the game-object
|
|
92
|
+
* path walk attributes components to the object that owns them and must read
|
|
93
|
+
* the same rule this index reads — a second collector is exactly how the two
|
|
94
|
+
* universes drift apart.
|
|
95
|
+
*
|
|
96
|
+
* `blockName` names the block `message` was taken from, because the `data:`
|
|
97
|
+
* re-parse is keyed off it: a whole document is `""`, while a single
|
|
98
|
+
* `embedded_instances` block has to say so or its payload is never opened.
|
|
99
|
+
*/
|
|
100
|
+
export function collectComponentDeclarations(
|
|
101
|
+
message: SceneMessage,
|
|
102
|
+
displayPath: string,
|
|
103
|
+
incomplete: string[],
|
|
104
|
+
blockName = "",
|
|
105
|
+
): ComponentDeclarations {
|
|
106
|
+
const declarations: ComponentDeclarations = { ids: new Set(), resources: new Map() };
|
|
107
|
+
collect(message, blockName, 0, displayPath, declarations, incomplete);
|
|
108
|
+
return declarations;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The id half of {@link collectComponentDeclarations}, for callers that join no resources. */
|
|
112
|
+
export function collectComponentIds(
|
|
113
|
+
message: SceneMessage,
|
|
114
|
+
displayPath: string,
|
|
115
|
+
incomplete: string[],
|
|
116
|
+
blockName = "",
|
|
117
|
+
): Set<string> {
|
|
118
|
+
return collectComponentDeclarations(message, displayPath, incomplete, blockName).ids;
|
|
119
|
+
}
|
|
120
|
+
|
|
55
121
|
// Build the component-id universe from already-read scene sources: keys are
|
|
56
122
|
// display paths, values are file text. Pure — the filesystem walk belongs to the
|
|
57
123
|
// caller, so a test can drive this from inline strings and a build can drive it
|
|
@@ -69,7 +135,9 @@ export function buildSceneComponentIndex(
|
|
|
69
135
|
|
|
70
136
|
for (const [displayPath, text] of documents) {
|
|
71
137
|
try {
|
|
72
|
-
|
|
138
|
+
for (const id of collectComponentIds(parseSceneTextFormat(text), displayPath, incomplete)) {
|
|
139
|
+
ids.add(id);
|
|
140
|
+
}
|
|
73
141
|
} catch (error) {
|
|
74
142
|
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
75
143
|
incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { readLibrarySceneDocuments } from "./library-dependencies";
|
|
2
|
+
import { isDefignoredPath } from "./project-resources";
|
|
3
|
+
|
|
4
|
+
// The narrow slice of `ts.server.ServerHost` this needs: the real host
|
|
5
|
+
// satisfies it structurally, and a test fake is two methods rather than a whole
|
|
6
|
+
// server. `readDirectory` is optional because a host may not provide it, and a
|
|
7
|
+
// plugin that throws takes the editor's completions down with it.
|
|
8
|
+
export interface SceneReadHost {
|
|
9
|
+
readDirectory?(path: string, extensions?: readonly string[]): string[];
|
|
10
|
+
readFile(path: string): string | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SCENE_EXTENSIONS = [".go", ".collection"];
|
|
14
|
+
|
|
15
|
+
// The extension sets more than one reader asks the host for. They live here
|
|
16
|
+
// rather than beside either caller because the index cache serves exactly the
|
|
17
|
+
// sets it is handed: a second definition that drifted would walk — and watch —
|
|
18
|
+
// a universe the other reader never invalidates.
|
|
19
|
+
export const GUI_EXTENSIONS = [".gui"];
|
|
20
|
+
export const INPUT_BINDING_EXTENSIONS = [".input_binding"];
|
|
21
|
+
|
|
22
|
+
// The component files that name a collection without instancing it: a
|
|
23
|
+
// `.collectionproxy` opens it as its own world, a `.collectionfactory` spawns
|
|
24
|
+
// copies of it at runtime. Deliberately not folded into `SCENE_EXTENSIONS`,
|
|
25
|
+
// which would feed non-scene text to `buildSceneComponentIndex`.
|
|
26
|
+
export const COLLECTION_REFERENCE_EXTENSIONS = [".collectionproxy", ".collectionfactory"];
|
|
27
|
+
|
|
28
|
+
// The documents an animation id is declared by, plus every hop taken to reach
|
|
29
|
+
// one: an atlas or tile source names a sprite's ids outright, a `.animationset`
|
|
30
|
+
// names the files whose basenames are a model's ids, and a `.sprite` or
|
|
31
|
+
// `.model` is the hop a referenced component takes to reach either.
|
|
32
|
+
export const ANIMATION_ASSET_EXTENSIONS = [
|
|
33
|
+
".atlas",
|
|
34
|
+
".tilesource",
|
|
35
|
+
".sprite",
|
|
36
|
+
".model",
|
|
37
|
+
".animationset",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// The file kinds a library shares into the address universe: the documents that
|
|
41
|
+
// declare ids, plus the ones that *classify* a collection into the world it is —
|
|
42
|
+
// a library's own `.collectionproxy` is what says its collection is a proxy
|
|
43
|
+
// world rather than reached by nothing. `.input_binding` and `.project` are
|
|
44
|
+
// deliberately absent: `game.project` names exactly one active binding file, and
|
|
45
|
+
// a vendored `*.project` declares keys this project's readers cannot resolve —
|
|
46
|
+
// the rule `PROJECT_EXTENSIONS` states below.
|
|
47
|
+
//
|
|
48
|
+
// It lives here rather than beside its reader for the reason the sets above do,
|
|
49
|
+
// plus one more: `library-dependencies.ts` and this file import each other, and
|
|
50
|
+
// a constant built from these arrays at that module's top level would read them
|
|
51
|
+
// before this one had run.
|
|
52
|
+
export const LIBRARY_SCENE_EXTENSIONS = [
|
|
53
|
+
...SCENE_EXTENSIONS,
|
|
54
|
+
...GUI_EXTENSIONS,
|
|
55
|
+
...ANIMATION_ASSET_EXTENSIONS,
|
|
56
|
+
...COLLECTION_REFERENCE_EXTENSIONS,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// The walk returns every `.project` the project holds, so the lookup is by
|
|
60
|
+
// display path — a vendored `*.project` declares keys this project's readers
|
|
61
|
+
// cannot resolve.
|
|
62
|
+
export const PROJECT_EXTENSIONS = [".project"];
|
|
63
|
+
export const GAME_PROJECT_DOCUMENT = "game.project";
|
|
64
|
+
|
|
65
|
+
// The project-relative name a file is keyed by — the same name the editor
|
|
66
|
+
// reports for a program file, so a scene's claim on a script can be looked up
|
|
67
|
+
// against the file being edited.
|
|
68
|
+
export function displayPathOf(projectRoot: string, filePath: string): string {
|
|
69
|
+
const path = filePath.replace(/\\/g, "/");
|
|
70
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
71
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// What both walks exclude is what Defold does not load as a project resource:
|
|
75
|
+
// `build/` output, plus the directories the scaffolded `.defignore` names
|
|
76
|
+
// (`isDefignoredPath`, root-anchored because a `.defignore` line is a
|
|
77
|
+
// root-relative path).
|
|
78
|
+
//
|
|
79
|
+
// Bob writes `_generated_*.go` copies of the project's scenes under `build/`;
|
|
80
|
+
// reading them would report every id twice, against files no author edits. The
|
|
81
|
+
// segment test runs on the project-relative path, so a project that itself
|
|
82
|
+
// lives under a directory named `build` is not excluded wholesale — a different
|
|
83
|
+
// rule from `.defignore`'s, because bob writes those copies below whatever
|
|
84
|
+
// depth it is pointed at.
|
|
85
|
+
function isBuildOutput(displayPath: string): boolean {
|
|
86
|
+
return displayPath.split("/").includes("build");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The single rule, exported because the index cache filters watch events with
|
|
90
|
+
// it: a copy in the watcher could drift from what the walks exclude, and an
|
|
91
|
+
// event the walk would have ignored is exactly the event that must not throw the
|
|
92
|
+
// cache away — bob writes `_generated_*.go` throughout a build.
|
|
93
|
+
export function isExcludedProjectPath(displayPath: string): boolean {
|
|
94
|
+
return isBuildOutput(displayPath) || isDefignoredPath(displayPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Read the project's own scene sources through the editor host — by default the
|
|
98
|
+
// `.go`/`.collection` set the component-id universe is built from, or whatever
|
|
99
|
+
// `extensions` names instead. The two universes stay disjoint: a `.gui` declares
|
|
100
|
+
// node ids, not component ids, so folding it into the default would feed gui
|
|
101
|
+
// text to `buildSceneComponentIndex`. Keys are project-relative display paths,
|
|
102
|
+
// matching what `buildSceneComponentIndex` reports in `incomplete`, and every
|
|
103
|
+
// file that could not be read is named in `unreadable` — a universe with a
|
|
104
|
+
// silent hole cannot be reasoned about. `paths` names the host paths actually
|
|
105
|
+
// read — what a watcher must be registered on, which cannot be reconstructed
|
|
106
|
+
// from a display path for a file outside the project root.
|
|
107
|
+
//
|
|
108
|
+
// The universe is the project plus whatever `resolve` last materialized from its
|
|
109
|
+
// library dependencies: a library document joins it under the merged Defold
|
|
110
|
+
// resource path it is addressed by, with its origin recorded in `origins` rather
|
|
111
|
+
// than folded into the key, and the project's own file wins a collision. Which
|
|
112
|
+
// makes the universe only as current as the last `resolve` — so a dependency
|
|
113
|
+
// that is declared but unresolved, unreadable or shadowed is a named
|
|
114
|
+
// `unreadable` line, never a silent omission.
|
|
115
|
+
export function readSceneDocuments(
|
|
116
|
+
host: SceneReadHost,
|
|
117
|
+
projectRoot: string,
|
|
118
|
+
extensions: readonly string[] = SCENE_EXTENSIONS,
|
|
119
|
+
): {
|
|
120
|
+
documents: Map<string, string>;
|
|
121
|
+
origins: Map<string, string>;
|
|
122
|
+
unreadable: string[];
|
|
123
|
+
paths: string[];
|
|
124
|
+
} {
|
|
125
|
+
const documents = new Map<string, string>();
|
|
126
|
+
const origins = new Map<string, string>();
|
|
127
|
+
const unreadable: string[] = [];
|
|
128
|
+
const paths: string[] = [];
|
|
129
|
+
|
|
130
|
+
if (!host.readDirectory) {
|
|
131
|
+
unreadable.push("the editor host cannot enumerate project files, so no scene source was read");
|
|
132
|
+
return { documents, origins, unreadable, paths };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const filePath of host.readDirectory(projectRoot, extensions)) {
|
|
136
|
+
const displayPath = displayPathOf(projectRoot, filePath);
|
|
137
|
+
if (isExcludedProjectPath(displayPath)) continue;
|
|
138
|
+
const text = host.readFile(filePath);
|
|
139
|
+
if (text === undefined) {
|
|
140
|
+
unreadable.push(`${displayPath}: could not be read`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
documents.set(displayPath, text);
|
|
144
|
+
paths.push(filePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const library = readLibrarySceneDocuments(host, projectRoot, extensions);
|
|
148
|
+
unreadable.push(...library.unreadable);
|
|
149
|
+
for (const { displayPath, hostPath, url, text } of library.documents) {
|
|
150
|
+
if (documents.has(displayPath)) {
|
|
151
|
+
unreadable.push(`${displayPath}: also declared by ${url}; the project's own file is used`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
documents.set(displayPath, text);
|
|
155
|
+
origins.set(displayPath, url);
|
|
156
|
+
paths.push(hostPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { documents, origins, unreadable, paths };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The project's own files of the given kinds, as the `/`-prefixed
|
|
163
|
+
// project-relative paths a Defold resource property names. Nothing is read: a
|
|
164
|
+
// resource path is the whole suggestion, so the walk is the entire universe.
|
|
165
|
+
//
|
|
166
|
+
// The extension filter is re-applied rather than trusted. `readSceneDocuments`
|
|
167
|
+
// can take the host at its word because a wrong document merely contributes no
|
|
168
|
+
// ids, but here a wrong file *is* the suggestion — a host that ignored its
|
|
169
|
+
// `extensions` argument would turn a `.font` slot into a project-wide file dump.
|
|
170
|
+
export function listProjectResourcePaths(
|
|
171
|
+
host: SceneReadHost,
|
|
172
|
+
projectRoot: string,
|
|
173
|
+
extensions: readonly string[],
|
|
174
|
+
): Set<string> {
|
|
175
|
+
if (!host.readDirectory) return new Set();
|
|
176
|
+
|
|
177
|
+
const paths: string[] = [];
|
|
178
|
+
for (const filePath of host.readDirectory(projectRoot, extensions)) {
|
|
179
|
+
const displayPath = displayPathOf(projectRoot, filePath);
|
|
180
|
+
if (isExcludedProjectPath(displayPath)) continue;
|
|
181
|
+
if (!extensions.some((extension) => displayPath.endsWith(extension))) continue;
|
|
182
|
+
paths.push(`/${displayPath}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return new Set(paths.sort());
|
|
186
|
+
}
|