@ringozz/godot 4.7.2-626 → 4.7.2-628

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/src/load.ts CHANGED
@@ -1,231 +1,271 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
4
-
5
- import { DirAccess } from '../gen/classes/DirAccess.ts';
6
- import { FileAccess } from '../gen/classes/FileAccess.ts';
7
- import { ProjectSettings } from '../gen/classes/ProjectSettings.ts';
8
- import { ResourceLoader, ThreadLoadStatus } from '../gen/classes/ResourceLoader.ts';
9
- import { ResourceUID } from '../gen/classes/ResourceUID.ts';
10
- import type { Resource } from '../gen/classes/Resource.ts';
11
- import { cleanupHooks, stageFile } from './runtime.ts';
12
- import { decodeCtex } from './web-image.ts';
13
-
14
- // Each loaded wrapper is released at shutdown via a `cleanupHooks` entry
15
- // holding a WeakRef: module-scope asset imports (the preload modules' memoized
16
- // load promises) keep their wrappers reachable, so GC alone can't collect them
17
- // before the engine stops. WeakRef keeps the entries from pinning resources
18
- // during the app's lifetime.
19
- function track<T extends Resource>(res: T): T {
20
- const ref = new WeakRef<Resource>(res);
21
- cleanupHooks.add(() => {
22
- const r = ref.deref();
23
- if (r) {
24
- try { r.free(); } catch {}
25
- }
26
- });
27
- return res;
28
- }
29
-
30
- /**
31
- * A Godot `Resource` subclass constructor: its `.name` is the registered class
32
- * name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
33
- * loaded resource type.
34
- */
35
- type ResourceConstructor = { new(...args: any[]): Resource } & Function;
36
-
37
- /**
38
- * A file to stage before loading, keyed by `res://` path: sidecars (`.import`)
39
- * arrive as `content` (text); imported products (`.scn`/`.ctex`) arrive as
40
- * `path` (fetched).
41
- */
42
- interface AssetFile {
43
- content?: string;
44
- path?: string;
45
- }
46
-
47
- export type AssetFiles = Record<string, AssetFile>;
48
-
49
- const UID_RE = /uid="(uid:\/\/[\w.]+)"/;
50
-
51
- // Godot encodes `uid://` numbers in base 34 over `a-y` then `0-8`
52
- // (core/io/resource_uid.cpp). Decode in JS with BigInt: `ResourceUID.textToId`
53
- // returns a JS number, and these ids exceed `Number.MAX_SAFE_INTEGER`, so the
54
- // low digits would be lost. BigInt flows through `_C` losslessly (int64).
55
- const UID_CHARS = 'abcdefghijklmnopqrstuvwxy012345678';
56
-
57
- function uidToId(uid: string): bigint {
58
- let id = 0n;
59
- for (let i = 6; i < uid.length; i++) {
60
- id = id * 34n + BigInt(UID_CHARS.indexOf(uid[i]));
61
- }
62
- return id;
63
- }
64
-
65
- /**
66
- * Registers an asset's `uid://` → `res://` path so scene ext_resource references
67
- * resolve it (Godot's `ResourceUID` map) instead of warning and falling back to
68
- * the stored text path. Idempotent; skips unknown/malformed uids.
69
- */
70
- function registerAssetUid(uid: string, resPath: string): void {
71
- const id = uidToId(uid);
72
- if (id !== 0n && !ResourceUID.hasId(id as unknown as number)) {
73
- ResourceUID.addId(id as unknown as number, resPath);
74
- }
75
- }
76
-
77
- async function materialize([resPath, entry]: [string, AssetFile]): Promise<void> {
78
- if (FileAccess.fileExists(resPath)) {
79
- return;
80
- }
81
- let bytes: Uint8Array;
82
- if (entry.content !== undefined) {
83
- // Sidecars (`.import`) and native text sources carry the asset's `uid=`;
84
- // register it so scenes resolve by uid. `.import` maps to the source path
85
- // (resPath minus the suffix); native files map to themselves.
86
- const uid = entry.content.match(UID_RE)?.[1];
87
- if (uid) {
88
- registerAssetUid(uid, resPath.endsWith('.import') ? resPath.slice(0, -'.import'.length) : resPath);
89
- }
90
- bytes = new TextEncoder().encode(entry.content);
91
- } else if (entry.path) {
92
- const res = await fetch(entry.path);
93
- if (!res.ok) {
94
- return;
95
- }
96
- bytes = new Uint8Array(await res.arrayBuffer());
97
- if (resPath.endsWith('.ctex')) {
98
- // On web, decode embedded PNG/WebP blobs with the browser before
99
- // staging, so the engine never runs an image codec (see web-image.ts).
100
- bytes = await decodeCtex(bytes);
101
- }
102
- } else {
103
- return;
104
- }
105
- stageFile?.(ProjectSettings.globalizePath(resPath), bytes);
106
- }
107
-
108
- const nextTick = () => new Promise(requestAnimationFrame);
109
-
110
- /**
111
- * An asset module as generated by `@ringozz/godot/preload`: `default` is the
112
- * load promise, `materialize` resolves once its bundled files (plus its deps'
113
- * files) are staged on the engine's filesystem.
114
- */
115
- interface AssetModule {
116
- default: Promise<unknown>;
117
- materialize: Promise<unknown>;
118
- }
119
-
120
- /**
121
- * Stages the asset's bundled files before loading: `.import` sidecars as text
122
- * and imported products (`.scn`/`.ctex`) as fetched paths. On web the bytes are
123
- * written straight into Emscripten's MEMFS via Godot's `copyToFS` (JS-heap only,
124
- * no wasm copy); a no-op on desktop where the files already exist. `deps`
125
- * supplies referenced asset modules whose file-staging is awaited (so Godot can
126
- * resolve them) and whose load failures are logged.
127
- */
128
- function materializeFiles(files: AssetFiles, deps: AssetModule[] = []): Promise<unknown> {
129
- for (const dep of deps) {
130
- dep.default.catch((err) => console.error('[godot] dependency load failed:', err));
131
- }
132
- if (stageFile === undefined) {
133
- // Desktop: the files already exist on disk (res:// = cwd) and uids come
134
- // from `.godot/uid_cache.bin`, so there is nothing to stage.
135
- return Promise.resolve();
136
- }
137
- return Promise.all([...Object.entries(files).map(materialize), ...deps.map((dep) => dep.materialize)]);
138
- }
139
-
140
- /**
141
- * Returns a promise resolving to the resource already cached in the engine at
142
- * `path` (`ResourceCache`), or `null` when it isn't loaded yet. Used by
143
- * {@link loadAsset} to skip file staging and the threaded load when a
144
- * re-evaluated module (e.g. web HMR) references an asset that is still cached.
145
- * `getCachedRef` returns the same JS wrapper as `loadThreadedGet` (instance
146
- * binding), so identity is preserved.
147
- */
148
- function cachedResource<C extends ResourceConstructor>(path: string): Promise<InstanceType<C>> | null {
149
- const result = ResourceLoader.getCachedRef(path) as InstanceType<C>;
150
- return result ? Promise.resolve(result) : null;
151
- }
152
-
153
- /**
154
- * Entry point for the generated asset modules (`@ringozz/godot/preload`): checks
155
- * the engine's `ResourceCache` once (`cachedResource`), stages the bundled files
156
- * via `materializeFiles` when needed, then loads via {@link loadResourceAsync},
157
- * memoizing the resulting load promise on `data` (the module's
158
- * `import.meta.hot.data`, carried across HMR re-evaluations; `{}` on desktop
159
- * where modules evaluate once). Memoization keeps `use()` seeing the **same**
160
- * fulfilled promise object across re-evaluations — no Suspense fallback flash on
161
- * hot reload; a rejected load is evicted so the next evaluation retries. Returns
162
- * the `materialize` promise (own + deps' files staged) and the load promise.
163
- */
164
- export function loadAsset<C extends ResourceConstructor>(
165
- path: string,
166
- cls: C,
167
- files: AssetFiles,
168
- deps: AssetModule[] = [],
169
- data: Record<string, unknown> = {},
170
- ): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
171
- const cached = cachedResource<C>(path);
172
- if (cached) cached.then(track);
173
- const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
174
- const existing = data[path] as Promise<InstanceType<C>> | undefined;
175
- const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
176
- data[path] = load;
177
- load.catch(() => {
178
- if (data[path] === load) delete data[path];
179
- });
180
- return { materialize, load };
181
- }
182
-
183
- /**
184
- * Loads a resource in the background. `cls` supplies both the `ResourceLoader`
185
- * type hint (its registered `.name`) and the return type. On web, call
186
- * {@link materializeFiles} with the asset's bundled files first; on desktop the
187
- * files already exist. Pass the same `files` map to have the staged files
188
- * deleted from MEMFS once the resource is loaded (the resource stays cached in
189
- * the engine, so the bytes are no longer needed).
190
- */
191
- export async function loadResourceAsync<C extends ResourceConstructor>(
192
- path: string,
193
- cls: C,
194
- files?: AssetFiles,
195
- ): Promise<InstanceType<C>> {
196
- const err = ResourceLoader.loadThreadedRequest(path, cls.name);
197
- if (err) {
198
- throw new Error(`loadResourceAsync(${path}): loadThreadedRequest failed (${err})`);
199
- }
200
-
201
- let result: InstanceType<C>;
202
- while (true) {
203
- const status = ResourceLoader.loadThreadedGetStatus(path);
204
- if (status === ThreadLoadStatus.THREAD_LOAD_LOADED) {
205
- result = ResourceLoader.loadThreadedGet(path) as InstanceType<C>;
206
- break;
207
- }
208
- if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
209
- // Collect the engine's LoadToken so a failed load doesn't leave it
210
- // registered (a bare `RefCounted` leaked at exit). Safe no-op when
211
- // no token exists.
212
- ResourceLoader.loadThreadedGet(path);
213
- throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
214
- }
215
- await nextTick();
216
- }
217
- if (files && stageFile !== undefined) {
218
- // Keep `.import` sidecars — they route imported source paths to their
219
- // products (ResourceFormatImporter recognizes a path by its sidecar).
220
- // Delete everything else: the products are read only on a cache miss, and
221
- // the resource stays cached after loading, so the bytes are dead weight.
222
- // Best-effort: a file may already be gone (another module's cleanup or a
223
- // cache-miss re-stage).
224
- for (const resPath of Object.keys(files)) {
225
- if (!resPath.endsWith('.import')) {
226
- DirAccess.removeAbsolute(resPath);
227
- }
228
- }
229
- }
230
- return track(result);
231
- }
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
4
+
5
+ import { DirAccess } from '../gen/classes/DirAccess.ts';
6
+ import { FileAccess } from '../gen/classes/FileAccess.ts';
7
+ import { ProjectSettings } from '../gen/classes/ProjectSettings.ts';
8
+ import { ResourceLoader, ThreadLoadStatus } from '../gen/classes/ResourceLoader.ts';
9
+ import { ResourceUID } from '../gen/classes/ResourceUID.ts';
10
+ import type { Resource } from '../gen/classes/Resource.ts';
11
+ import { cleanupHooks, stageFile } from './runtime.ts';
12
+ import { transcoder } from '@ringozz/godot/transcoder';
13
+ import { decodeCtex } from './web-image.ts';
14
+
15
+ /**
16
+ * Produces the bytes staged for a `.ctex`: the platform transcoder (resolved
17
+ * once by `@ringozz/godot/transcoder`) when it yields a result, otherwise the raw
18
+ * RGBA8 browser decode. Never throws — a failed transcode degrades to the
19
+ * uncompressed path.
20
+ */
21
+ async function transcodeCtex(bytes: Uint8Array): Promise<Uint8Array> {
22
+ const encode = await transcoder;
23
+ if (encode) {
24
+ try {
25
+ const out = await encode(bytes);
26
+ if (out) {
27
+ return out;
28
+ }
29
+ } catch (err) {
30
+ console.warn('[godot] texture transcode failed; using raw RGBA8:', err);
31
+ }
32
+ }
33
+ return decodeCtex(bytes);
34
+ }
35
+
36
+ // Each loaded wrapper is released at shutdown via a `cleanupHooks` entry
37
+ // holding a WeakRef: module-scope asset imports (the preload modules' memoized
38
+ // load promises) keep their wrappers reachable, so GC alone can't collect them
39
+ // before the engine stops. WeakRef keeps the entries from pinning resources
40
+ // during the app's lifetime.
41
+ function track<T extends Resource>(res: T): T {
42
+ const ref = new WeakRef<Resource>(res);
43
+ cleanupHooks.add(() => {
44
+ const r = ref.deref();
45
+ if (r) {
46
+ try { r.free(); } catch {}
47
+ }
48
+ });
49
+ return res;
50
+ }
51
+
52
+ /**
53
+ * A Godot `Resource` subclass constructor: its `.name` is the registered class
54
+ * name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
55
+ * loaded resource type.
56
+ */
57
+ type ResourceConstructor = { new(...args: any[]): Resource } & Function;
58
+
59
+ /**
60
+ * A file to stage before loading, keyed by `res://` path: sidecars (`.import`)
61
+ * arrive as `content` (text); imported products (`.scn`/`.ctex`) arrive as
62
+ * `path` (fetched).
63
+ */
64
+ interface AssetFile {
65
+ content?: string;
66
+ path?: string;
67
+ }
68
+
69
+ export type AssetFiles = Record<string, AssetFile>;
70
+
71
+ const UID_RE = /uid="(uid:\/\/[\w.]+)"/;
72
+
73
+ // Compressed-texture products whose payloads may need decoding/transcoding:
74
+ // `.ctex` (CompressedTexture2D) and the layered `.ctexarray` / `.ccube` /
75
+ // `.ccubearray` (2D array / cubemap / cubemap array). `.ctex3d` has its own
76
+ // layout and is left to the engine.
77
+ const COMPRESSED_TEXTURE_RE = /\.(ctex|ctexarray|ccube|ccubearray)$/;
78
+
79
+ // Textures the runtime must receive *exactly* as the server served them, so they are decoded to raw RGBA8
80
+ // instead of going through the platform transcoder (spark -> BC7/ASTC): the terrain height atlas
81
+ // (`<level>.dat/terrain/height.webp`), whose 12-bit packed R/G the vertex shader decodes per texel, so
82
+ // block quantisation shows as blocky relief on every slope. Everything else may be compressed — the
83
+ // surface arrays, the weight planes and the `_n` tile normal all ship as layered containers the runtime
84
+ // only samples (see `docs/godot.md`'s M4 section). The `.ctex` product is named after its source plus an
85
+ // md5, which is what the pattern keys on.
86
+ const EXACT_TEXTURE_RE = /(^|\/)height\.webp-[0-9a-f]+\.ctex$/;
87
+
88
+ // Godot encodes `uid://` numbers in base 34 over `a-y` then `0-8`
89
+ // (core/io/resource_uid.cpp). Decode in JS with BigInt: `ResourceUID.textToId`
90
+ // returns a JS number, and these ids exceed `Number.MAX_SAFE_INTEGER`, so the
91
+ // low digits would be lost. BigInt flows through `_C` losslessly (int64).
92
+ const UID_CHARS = 'abcdefghijklmnopqrstuvwxy012345678';
93
+
94
+ function uidToId(uid: string): bigint {
95
+ let id = 0n;
96
+ for (let i = 6; i < uid.length; i++) {
97
+ id = id * 34n + BigInt(UID_CHARS.indexOf(uid[i]));
98
+ }
99
+ return id;
100
+ }
101
+
102
+ /**
103
+ * Registers an asset's `uid://` → `res://` path so scene ext_resource references
104
+ * resolve it (Godot's `ResourceUID` map) instead of warning and falling back to
105
+ * the stored text path. Idempotent; skips unknown/malformed uids.
106
+ */
107
+ function registerAssetUid(uid: string, resPath: string): void {
108
+ const id = uidToId(uid);
109
+ if (id !== 0n && !ResourceUID.hasId(id as unknown as number)) {
110
+ ResourceUID.addId(id as unknown as number, resPath);
111
+ }
112
+ }
113
+
114
+ async function materialize([resPath, entry]: [string, AssetFile]): Promise<void> {
115
+ if (FileAccess.fileExists(resPath)) {
116
+ return;
117
+ }
118
+ let bytes: Uint8Array;
119
+ if (entry.content !== undefined) {
120
+ // Sidecars (`.import`) and native text sources carry the asset's `uid=`;
121
+ // register it so scenes resolve by uid. `.import` maps to the source path
122
+ // (resPath minus the suffix); native files map to themselves.
123
+ const uid = entry.content.match(UID_RE)?.[1];
124
+ if (uid) {
125
+ registerAssetUid(uid, resPath.endsWith('.import') ? resPath.slice(0, -'.import'.length) : resPath);
126
+ }
127
+ bytes = new TextEncoder().encode(entry.content);
128
+ } else if (entry.path) {
129
+ const res = await fetch(entry.path);
130
+ if (!res.ok) {
131
+ return;
132
+ }
133
+ bytes = new Uint8Array(await res.arrayBuffer());
134
+ if (COMPRESSED_TEXTURE_RE.test(resPath)) {
135
+ // On web, decode embedded PNG/WebP blobs with the browser before
136
+ // staging, so the engine never runs an image codec. A registered
137
+ // transcoder (e.g. spark.js) may instead rebuild the container as a
138
+ // device-native block-compressed texture by the built-in transcoder —
139
+ // except for `EXACT_TEXTURE_RE`'s routes, which must stay raw (see there).
140
+ bytes = EXACT_TEXTURE_RE.test(resPath) ? await decodeCtex(bytes) : await transcodeCtex(bytes);
141
+ }
142
+ } else {
143
+ return;
144
+ }
145
+ stageFile?.(ProjectSettings.globalizePath(resPath), bytes);
146
+ }
147
+
148
+ const nextTick = () => new Promise(requestAnimationFrame);
149
+
150
+ /**
151
+ * An asset module as generated by `@ringozz/godot/preload`: `default` is the
152
+ * load promise, `materialize` resolves once its bundled files (plus its deps'
153
+ * files) are staged on the engine's filesystem.
154
+ */
155
+ interface AssetModule {
156
+ default: Promise<unknown>;
157
+ materialize: Promise<unknown>;
158
+ }
159
+
160
+ /**
161
+ * Stages the asset's bundled files before loading: `.import` sidecars as text
162
+ * and imported products (`.scn`/`.ctex`) as fetched paths. On web the bytes are
163
+ * written straight into Emscripten's MEMFS via Godot's `copyToFS` (JS-heap only,
164
+ * no wasm copy); a no-op on desktop where the files already exist. `deps`
165
+ * supplies referenced asset modules whose file-staging is awaited (so Godot can
166
+ * resolve them) and whose load failures are logged.
167
+ */
168
+ function materializeFiles(files: AssetFiles, deps: AssetModule[] = []): Promise<unknown> {
169
+ for (const dep of deps) {
170
+ dep.default.catch((err) => console.error('[godot] dependency load failed:', err));
171
+ }
172
+ if (stageFile === undefined) {
173
+ // Desktop: the files already exist on disk (res:// = cwd) and uids come
174
+ // from `.godot/uid_cache.bin`, so there is nothing to stage.
175
+ return Promise.resolve();
176
+ }
177
+ return Promise.all([...Object.entries(files).map(materialize), ...deps.map((dep) => dep.materialize)]);
178
+ }
179
+
180
+ /**
181
+ * Returns a promise resolving to the resource already cached in the engine at
182
+ * `path` (`ResourceCache`), or `null` when it isn't loaded yet. Used by
183
+ * {@link loadAsset} to skip file staging and the threaded load when a
184
+ * re-evaluated module (e.g. web HMR) references an asset that is still cached.
185
+ * `getCachedRef` returns the same JS wrapper as `loadThreadedGet` (instance
186
+ * binding), so identity is preserved.
187
+ */
188
+ function cachedResource<C extends ResourceConstructor>(path: string): Promise<InstanceType<C>> | null {
189
+ const result = ResourceLoader.getCachedRef(path) as InstanceType<C>;
190
+ return result ? Promise.resolve(result) : null;
191
+ }
192
+
193
+ /**
194
+ * Entry point for the generated asset modules (`@ringozz/godot/preload`): checks
195
+ * the engine's `ResourceCache` once (`cachedResource`), stages the bundled files
196
+ * via `materializeFiles` when needed, then loads via {@link loadResourceAsync},
197
+ * memoizing the resulting load promise on `data` (the module's
198
+ * `import.meta.hot.data`, carried across HMR re-evaluations; `{}` on desktop
199
+ * where modules evaluate once). Memoization keeps `use()` seeing the **same**
200
+ * fulfilled promise object across re-evaluations — no Suspense fallback flash on
201
+ * hot reload; a rejected load is evicted so the next evaluation retries. Returns
202
+ * the `materialize` promise (own + deps' files staged) and the load promise.
203
+ */
204
+ export function loadAsset<C extends ResourceConstructor>(
205
+ path: string,
206
+ cls: C,
207
+ files: AssetFiles,
208
+ deps: AssetModule[] = [],
209
+ data: Record<string, unknown> = {},
210
+ ): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
211
+ const cached = cachedResource<C>(path);
212
+ if (cached) cached.then(track);
213
+ const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
214
+ const existing = data[path] as Promise<InstanceType<C>> | undefined;
215
+ const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
216
+ data[path] = load;
217
+ load.catch(() => {
218
+ if (data[path] === load) delete data[path];
219
+ });
220
+ return { materialize, load };
221
+ }
222
+
223
+ /**
224
+ * Loads a resource in the background. `cls` supplies both the `ResourceLoader`
225
+ * type hint (its registered `.name`) and the return type. On web, call
226
+ * {@link materializeFiles} with the asset's bundled files first; on desktop the
227
+ * files already exist. Pass the same `files` map to have the staged files
228
+ * deleted from MEMFS once the resource is loaded (the resource stays cached in
229
+ * the engine, so the bytes are no longer needed).
230
+ */
231
+ export async function loadResourceAsync<C extends ResourceConstructor>(
232
+ path: string,
233
+ cls: C,
234
+ files?: AssetFiles,
235
+ ): Promise<InstanceType<C>> {
236
+ const err = ResourceLoader.loadThreadedRequest(path, cls.name);
237
+ if (err) {
238
+ throw new Error(`loadResourceAsync(${path}): loadThreadedRequest failed (${err})`);
239
+ }
240
+
241
+ let result: InstanceType<C>;
242
+ while (true) {
243
+ const status = ResourceLoader.loadThreadedGetStatus(path);
244
+ if (status === ThreadLoadStatus.THREAD_LOAD_LOADED) {
245
+ result = ResourceLoader.loadThreadedGet(path) as InstanceType<C>;
246
+ break;
247
+ }
248
+ if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
249
+ // Collect the engine's LoadToken so a failed load doesn't leave it
250
+ // registered (a bare `RefCounted` leaked at exit). Safe no-op when
251
+ // no token exists.
252
+ ResourceLoader.loadThreadedGet(path);
253
+ throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
254
+ }
255
+ await nextTick();
256
+ }
257
+ if (files && stageFile !== undefined) {
258
+ // Keep `.import` sidecars — they route imported source paths to their
259
+ // products (ResourceFormatImporter recognizes a path by its sidecar).
260
+ // Delete everything else: the products are read only on a cache miss, and
261
+ // the resource stays cached after loading, so the bytes are dead weight.
262
+ // Best-effort: a file may already be gone (another module's cleanup or a
263
+ // cache-miss re-stage).
264
+ for (const resPath of Object.keys(files)) {
265
+ if (!resPath.endsWith('.import')) {
266
+ DirAccess.removeAbsolute(resPath);
267
+ }
268
+ }
269
+ }
270
+ return track(result);
271
+ }