@symbiote-native/asset 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/angular/services/assets.service/index.d.ts +8 -0
  6. package/build/angular/services/assets.service/index.js +64 -0
  7. package/build/core/asset-fx.d.ts +1 -0
  8. package/build/core/asset-fx.js +24 -0
  9. package/build/core/asset-sources.d.ts +13 -0
  10. package/build/core/asset-sources.js +50 -0
  11. package/build/core/asset-uris.d.ts +3 -0
  12. package/build/core/asset-uris.js +33 -0
  13. package/build/core/asset.d.ts +35 -0
  14. package/build/core/asset.js +158 -0
  15. package/build/core/index.d.ts +3 -0
  16. package/build/core/index.js +2 -0
  17. package/build/core/local-assets.d.ts +1 -0
  18. package/build/core/local-assets.js +16 -0
  19. package/build/core/native-module.d.ts +4 -0
  20. package/build/core/native-module.js +2 -0
  21. package/build/core/platform-utils.d.ts +5 -0
  22. package/build/core/platform-utils.js +28 -0
  23. package/build/react/hooks/use-assets/index.d.ts +2 -0
  24. package/build/react/hooks/use-assets/index.js +14 -0
  25. package/build/react/index.d.ts +2 -0
  26. package/build/react/index.js +3 -0
  27. package/build/solid/index.d.ts +2 -0
  28. package/build/solid/index.js +5 -0
  29. package/build/solid/primitives/create-assets.d.ts +6 -0
  30. package/build/solid/primitives/create-assets.js +10 -0
  31. package/build/svelte/index.d.ts +2 -0
  32. package/build/svelte/index.js +5 -0
  33. package/build/svelte/runes/use-assets.svelte.d.ts +5 -0
  34. package/build/svelte/runes/use-assets.svelte.js +24 -0
  35. package/build/vue/composables/use-assets/index.d.ts +6 -0
  36. package/build/vue/composables/use-assets/index.js +18 -0
  37. package/build/vue/index.d.ts +2 -0
  38. package/build/vue/index.js +3 -0
  39. package/build-ngc/angular/index.d.ts +2 -0
  40. package/build-ngc/angular/index.js +5 -0
  41. package/build-ngc/angular/index.js.map +1 -0
  42. package/build-ngc/angular/services/assets.service/index.d.ts +11 -0
  43. package/build-ngc/angular/services/assets.service/index.js +24 -0
  44. package/build-ngc/angular/services/assets.service/index.js.map +1 -0
  45. package/build-ngc/core/asset-fx.d.ts +1 -0
  46. package/build-ngc/core/asset-fx.js +25 -0
  47. package/build-ngc/core/asset-fx.js.map +1 -0
  48. package/build-ngc/core/asset-sources.d.ts +13 -0
  49. package/build-ngc/core/asset-sources.js +51 -0
  50. package/build-ngc/core/asset-sources.js.map +1 -0
  51. package/build-ngc/core/asset-uris.d.ts +3 -0
  52. package/build-ngc/core/asset-uris.js +34 -0
  53. package/build-ngc/core/asset-uris.js.map +1 -0
  54. package/build-ngc/core/asset.d.ts +35 -0
  55. package/build-ngc/core/asset.js +159 -0
  56. package/build-ngc/core/asset.js.map +1 -0
  57. package/build-ngc/core/index.d.ts +3 -0
  58. package/build-ngc/core/index.js +3 -0
  59. package/build-ngc/core/index.js.map +1 -0
  60. package/build-ngc/core/local-assets.d.ts +1 -0
  61. package/build-ngc/core/local-assets.js +17 -0
  62. package/build-ngc/core/local-assets.js.map +1 -0
  63. package/build-ngc/core/native-module.d.ts +4 -0
  64. package/build-ngc/core/native-module.js +3 -0
  65. package/build-ngc/core/native-module.js.map +1 -0
  66. package/build-ngc/core/platform-utils.d.ts +5 -0
  67. package/build-ngc/core/platform-utils.js +29 -0
  68. package/build-ngc/core/platform-utils.js.map +1 -0
  69. package/native-link.json +12 -0
  70. package/package.json +151 -0
  71. package/src/angular/index.ts +4 -0
  72. package/src/angular/services/assets.service/index.ts +21 -0
  73. package/src/core/asset-fx.ts +30 -0
  74. package/src/core/asset-sources.ts +80 -0
  75. package/src/core/asset-uris.ts +51 -0
  76. package/src/core/asset.ts +212 -0
  77. package/src/core/index.ts +8 -0
  78. package/src/core/local-assets.ts +22 -0
  79. package/src/core/native-module.ts +11 -0
  80. package/src/core/platform-utils.ts +47 -0
  81. package/src/core/ts-declarations/react-native-assets.d.ts +75 -0
  82. package/src/react/hooks/use-assets/index.ts +19 -0
  83. package/src/react/index.ts +4 -0
  84. package/src/solid/index.ts +6 -0
  85. package/src/solid/primitives/create-assets.ts +16 -0
  86. package/src/svelte/index.ts +6 -0
  87. package/src/svelte/runes/use-assets.svelte.ts +30 -0
  88. package/src/svelte/svelte-ambient.d.ts +3 -0
  89. package/src/vue/composables/use-assets/index.ts +30 -0
  90. package/src/vue/index.ts +4 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @symbiote-native/asset
2
+
3
+ Turn `require('./logo.png')` (or a plain URI) into a local file: download it once, read
4
+ `localUri`, reuse it. One API for every [SymbioteNative](../../README.md) adapter (React, Vue,
5
+ Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-asset`](https://github.com/expo/expo/tree/main/packages/expo-asset): an `Asset`
8
+ instance resolved from a `require()` module id or URI, downloaded to a cache file. Built mainly so
9
+ [`@symbiote-native/font`](../font) can accept the same `number`/`Asset` font-source forms as
10
+ upstream `expo-font`; usable standalone too. Same recipe as
11
+ [`@symbiote-native/network`](../network), an `expo-modules-core`-based wrapper (see the
12
+ `symbiote-expo-native-module` project skill for the full mechanism).
13
+
14
+ ## Install
15
+
16
+ **New app:**
17
+
18
+ ```bash
19
+ npx @symbiote-native/cli new my-app --asset
20
+ ```
21
+
22
+ **Existing SymbioteNative app:**
23
+
24
+ ```bash
25
+ npx @symbiote-native/cli add --asset
26
+ ```
27
+
28
+ Either way: installs `@symbiote-native/asset` and wires the native autolinking automatically -
29
+ see [`@symbiote-native/cli`](../cli).
30
+
31
+ <details>
32
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
33
+
34
+ ```bash
35
+ npm install @symbiote-native/asset
36
+ ```
37
+
38
+ `expo-asset`, `expo-modules-core`, `expo-constants`, and `@react-native/assets-registry` come
39
+ along as regular dependencies, pinned to exact versions - never install `expo-asset` yourself, and
40
+ never add the `expo` meta-package to this project.
41
+
42
+ ### Required one-time step: native autolinking wiring
43
+
44
+ Same one-time step as every other `expo-modules-core` package this project ships - see
45
+ [`@symbiote-native/network`'s README](../network/README.md#required-one-time-step-native-autolinking-wiring)
46
+ and the `symbiote-expo-native-module` project skill. `native-link.json` registers one Android
47
+ module (`AssetModule` → `ExpoAsset`); iOS needs no manifest entry, it autolinks via
48
+ `use_expo_modules!`.
49
+
50
+ No platform permission string is needed - asset resolution and caching reads no protected system
51
+ state on either platform.
52
+
53
+ </details>
54
+
55
+ ## Shape
56
+
57
+ ```
58
+ src/core/ asset.ts - the Asset class (fromModule/fromMetadata/fromURI/loadAsync/
59
+ downloadAsync). asset-sources.ts - selectAssetSource/resolveUri (scale
60
+ pick, manifest2 dev-server resolution). asset-uris.ts -
61
+ getFilename/getFileExtension/getManifestBaseUrl. local-assets.ts -
62
+ getLocalAssetUri (Expo Go / expo-updates embedded assets).
63
+ platform-utils.ts - IS_ENV_WITH_LOCAL_ASSETS + the manifest/Constants
64
+ reads. asset-fx.ts - registers the Image custom source transformer as a
65
+ module-load side effect. native-module.ts resolves ExpoAsset through
66
+ expo-modules-core's requireNativeModule.
67
+ src/react/hooks/ @symbiote-native/asset/react - useAssets
68
+ src/vue/composables/ @symbiote-native/asset/vue - useAssets (same name)
69
+ src/svelte/runes/ @symbiote-native/asset/svelte - useAssets (same name)
70
+ src/solid/primitives/ @symbiote-native/asset/solid - createAssets (Solid reserves `use*` for
71
+ consuming existing state)
72
+ src/angular/services/ @symbiote-native/asset/angular - AssetsService (`.connect()` returns a
73
+ Signal pair)
74
+ ```
75
+
76
+ Each adapter's hook/composable/rune/primitive/service is a thin lifecycle wrapper over
77
+ `Asset.loadAsync`, matching upstream's own `useAssets` hook contract (loads once on mount).
78
+
79
+ ## Use it
80
+
81
+ ```tsx
82
+ // React
83
+ import { useAssets } from '@symbiote-native/asset/react';
84
+
85
+ function Logo() {
86
+ const [assets, error] = useAssets(require('./assets/logo.png'));
87
+ return assets ? <image source={{ uri: assets[0].localUri ?? undefined }} /> : null;
88
+ }
89
+ ```
90
+
91
+ ```vue
92
+ <!-- Vue -->
93
+ <script setup lang="ts">
94
+ import { useAssets } from '@symbiote-native/asset/vue';
95
+
96
+ const { assets, error } = useAssets(require('./assets/logo.png'));
97
+ </script>
98
+ <template>
99
+ <image v-if="assets" :source="{ uri: assets[0].localUri ?? undefined }" />
100
+ </template>
101
+ ```
102
+
103
+ ```svelte
104
+ <!-- Svelte -->
105
+ <script lang="ts">
106
+ import { useAssets } from '@symbiote-native/asset/svelte';
107
+
108
+ const { assets, error } = useAssets(require('./assets/logo.png'));
109
+ </script>
110
+
111
+ {#if assets}
112
+ <image source={{ uri: assets[0].localUri ?? undefined }} />
113
+ {/if}
114
+ ```
115
+
116
+ ```ts
117
+ // Angular
118
+ import { Component, inject } from '@angular/core';
119
+ import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
120
+ import { AssetsService } from '@symbiote-native/asset/angular';
121
+
122
+ @Component({
123
+ standalone: true,
124
+ imports: [SYMBIOTE_ELEMENTS],
125
+ template: `@if (assets.assets(); as loaded) {
126
+ <image [source]="{ uri: loaded[0].localUri ?? undefined }" />
127
+ }`,
128
+ })
129
+ export class Logo {
130
+ readonly assets = inject(AssetsService).connect(require('./assets/logo.png'));
131
+ }
132
+ ```
133
+
134
+ ```tsx
135
+ // Solid - the accessor is CALLED; a Solid component body runs once.
136
+ import { createAssets } from '@symbiote-native/asset/solid';
137
+
138
+ function Logo() {
139
+ const { assets } = createAssets(require('./assets/logo.png'));
140
+ const loaded = () => assets()?.[0];
141
+ return loaded() ? (
142
+ <image source={{ uri: loaded()!.localUri ?? undefined }} />
143
+ ) : null;
144
+ }
145
+ ```
146
+
147
+ ## API
148
+
149
+ ```ts
150
+ Asset.fromModule(virtualAssetModule: number | string | { uri: string; width: number; height: number }): Asset
151
+ Asset.fromMetadata(meta: AssetMetadata): Asset
152
+ Asset.fromURI(uri: string): Asset
153
+ Asset.loadAsync(moduleId: number | number[] | string | string[]): Promise<Asset[]>
154
+ asset.downloadAsync(): Promise<Asset>
155
+ ```
156
+
157
+ Plus `AssetDescriptor`, `AssetMetadata`, `AssetSource` - hand-ported from upstream's `Asset.ts`/
158
+ `AssetSources.ts`. `useAssets`/`createAssets` accept `number | number[]` only, matching upstream's
159
+ own `AssetHooks.ts` (narrower than `Asset.loadAsync`'s own `string`/`string[]` forms).
160
+
161
+ ## Full parity - Expo Go / expo-updates code paths included
162
+
163
+ Every native-side upstream module is ported, including the Expo Go / classic-updates /
164
+ `expo-updates` branches (`getLocalAssetUri` reading `ExpoUpdates.localAssets`, `AssetSources.ts`'s
165
+ manifest2 dev-server resolution, `Asset.fx.ts`'s custom `Image` source-transformer registration).
166
+ This repo ships neither Expo Go nor `expo-updates`, so `IS_ENV_WITH_LOCAL_ASSETS` is always `false`
167
+ in practice and those branches sit naturally inert - real, tested code (see
168
+ `platform-utils.test.ts`, `asset-fx.test.ts`, `local-assets.test.ts`), not stubs. If a consuming
169
+ app ever installs `expo-updates` itself, this package picks it up automatically, same as upstream.
170
+
171
+ ## Not ported
172
+
173
+ - **The `expo-asset` config plugin.** It links files into the native project from `app.json`; this
174
+ repo runs no Expo prebuild step. Bundle files with `require()` through Metro and add unusual
175
+ extensions to `assetExts` in the Metro config.
176
+ - **The web platform** - no `ImageAssets`/browser `Image()` probing; only the native path is
177
+ ported (same convention as every other package in this repo - see the root `CLAUDE.md`).
178
+
179
+ ## Common questions
180
+
181
+ - **`localUri` is `null`.** It is `null` until the asset is downloaded: read it after
182
+ `Asset.loadAsync` or `downloadAsync` resolves.
183
+ - **Preload several assets.** `await Asset.loadAsync([require('./a.png'), require('./b.png')])`.
184
+ - **Where does the file go?** The app's cache directory; the OS may remove it later.
185
+ - **Does the hook reload for a new module id?** No, it loads once on mount, like upstream.
186
+
187
+ Sources: [Expo docs: Asset](https://docs.expo.dev/versions/latest/sdk/asset/),
188
+ [expo/expo#8222](https://github.com/expo/expo/pull/8222).
189
+
190
+ ## Test it
191
+
192
+ No Fabric/Descriptor angle at all - every function here is a pure async-function surface over the
193
+ `Asset` class, never a view. Tests inject a fake native-module object in place of the real
194
+ `requireNativeModule` resolution (`src/core/{asset,asset-sources,asset-uris,local-assets,
195
+ platform-utils,asset-fx}.test.ts`, `src/{react,vue,svelte,solid,angular}/**/*.test.{ts,tsx}`) - no
196
+ `installFabric()`, no ViewConfig.
197
+
198
+ The `AssetScreen` in the `examples/expo-*` apps carries the on-device verification.
@@ -0,0 +1,2 @@
1
+ export { AssetsService } from './services/assets.service';
2
+ export * from '../core';
@@ -0,0 +1,4 @@
1
+ // AssetsService is the Angular-only lifecycle half; the Asset class and loading logic all live
2
+ // in core, shared with React/Vue/Svelte/Solid.
3
+ export { AssetsService } from './services/assets.service/index.js';
4
+ export * from '../core/index.js';
@@ -0,0 +1,8 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { Asset } from '../../../core';
3
+ export declare class AssetsService {
4
+ connect(moduleIds: number | number[]): {
5
+ assets: Signal<Asset[] | undefined>;
6
+ error: Signal<Error | undefined>;
7
+ };
8
+ }
@@ -0,0 +1,64 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { Injectable, signal } from '@angular/core';
36
+ import { Asset } from '../../../core/index.js';
37
+ // Angular twin of React's useAssets hook and Vue's useAssets composable — connect(ids) returns
38
+ // a Signal pair, e.g. `readonly assets = inject(AssetsService).connect([require('./a.jpg')])`.
39
+ let AssetsService = (() => {
40
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
41
+ let _classDescriptor;
42
+ let _classExtraInitializers = [];
43
+ let _classThis;
44
+ var AssetsService = class {
45
+ static { _classThis = this; }
46
+ static {
47
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
48
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
49
+ AssetsService = _classThis = _classDescriptor.value;
50
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
51
+ __runInitializers(_classThis, _classExtraInitializers);
52
+ }
53
+ connect(moduleIds) {
54
+ const assets = signal(undefined);
55
+ const error = signal(undefined);
56
+ Asset.loadAsync(moduleIds)
57
+ .then(loaded => assets.set(loaded))
58
+ .catch((err) => error.set(err));
59
+ return { assets: assets.asReadonly(), error: error.asReadonly() };
60
+ }
61
+ };
62
+ return AssetsService = _classThis;
63
+ })();
64
+ export { AssetsService };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ // Ported from expo-asset/src/Asset.fx.ts (sdk-57). Overrides RN's own Image asset resolution
2
+ // for a classic-updates/Expo-Go env — a no-op import in a bare app (IS_ENV_WITH_LOCAL_ASSETS
3
+ // is false), imported for its side effect from core/index.ts, matching upstream's own index.ts.
4
+ import resolveAssetSource, {} from 'react-native/Libraries/Image/resolveAssetSource';
5
+ import { Asset, ANDROID_EMBEDDED_URL_BASE_RESOURCE } from './asset.js';
6
+ import { IS_ENV_WITH_LOCAL_ASSETS } from './platform-utils.js';
7
+ const expoAssetTransformer = resolver => {
8
+ try {
9
+ if ('fileHashes' in resolver.asset && resolver.asset.fileHashes) {
10
+ const asset = Asset.fromMetadata(resolver.asset);
11
+ if (asset.uri.startsWith(ANDROID_EMBEDDED_URL_BASE_RESOURCE)) {
12
+ return resolver.resourceIdentifierWithoutScale();
13
+ }
14
+ return resolver.fromSource(asset.downloaded && asset.localUri ? asset.localUri : asset.uri);
15
+ }
16
+ return resolver.defaultAsset();
17
+ }
18
+ catch {
19
+ return resolver.defaultAsset();
20
+ }
21
+ };
22
+ if (IS_ENV_WITH_LOCAL_ASSETS) {
23
+ resolveAssetSource.setCustomSourceTransformer?.(expoAssetTransformer);
24
+ }
@@ -0,0 +1,13 @@
1
+ import type { PackagerAsset } from '@react-native/assets-registry/registry';
2
+ export type AssetMetadata = Pick<PackagerAsset, 'httpServerLocation' | 'name' | 'hash' | 'type' | 'scales' | 'width' | 'height'> & {
3
+ uri?: string;
4
+ fileHashes?: string[];
5
+ fileUris?: string[];
6
+ };
7
+ export type AssetSource = {
8
+ uri: string;
9
+ hash: string;
10
+ };
11
+ /** Resolves a relative asset URI against the classic-updates manifest base, if any. */
12
+ export declare function resolveUri(uri: string): string;
13
+ export declare function selectAssetSource(meta: AssetMetadata): AssetSource;
@@ -0,0 +1,50 @@
1
+ import { Platform } from 'expo-modules-core';
2
+ import { NativeModules, PixelRatio } from 'react-native';
3
+ import AssetSourceResolver from 'react-native/Libraries/Image/AssetSourceResolver';
4
+ import { getManifest2, manifestBaseUrl } from './platform-utils.js';
5
+ /** Resolves a relative asset URI against the classic-updates manifest base, if any. */
6
+ export function resolveUri(uri) {
7
+ return manifestBaseUrl ? new URL(uri, manifestBaseUrl).href : uri;
8
+ }
9
+ export function selectAssetSource(meta) {
10
+ const scale = AssetSourceResolver.pickScale(meta.scales, PixelRatio.get());
11
+ const index = meta.scales.findIndex(s => s === scale);
12
+ const hash = meta.fileHashes?.[index] ?? meta.fileHashes?.[0] ?? meta.hash;
13
+ const uri = meta.fileUris
14
+ ? (meta.fileUris[index] ?? meta.fileUris[0])
15
+ : meta.uri;
16
+ if (uri) {
17
+ return { uri: resolveUri(uri), hash };
18
+ }
19
+ const fileScale = scale === 1 ? '' : `@${scale}x`;
20
+ const fileExtension = meta.type ? `.${encodeURIComponent(meta.type)}` : '';
21
+ const suffix = `/${encodeURIComponent(meta.name)}${fileScale}${fileExtension}`;
22
+ const params = new URLSearchParams({
23
+ platform: Platform.OS,
24
+ hash: meta.hash,
25
+ });
26
+ if (/^https?:\/\//.test(meta.httpServerLocation)) {
27
+ return { uri: meta.httpServerLocation + suffix + '?' + params, hash };
28
+ }
29
+ const manifest2 = getManifest2();
30
+ const scheme = manifestBaseUrl?.startsWith('https://')
31
+ ? 'https://'
32
+ : 'http://';
33
+ const devServerUrl = manifest2?.extra?.expoGo?.developer
34
+ ? scheme + manifest2.extra.expoGo.debuggerHost
35
+ : null;
36
+ if (devServerUrl) {
37
+ const baseUrl = new URL(meta.httpServerLocation + suffix, devServerUrl);
38
+ baseUrl.searchParams.set('platform', Platform.OS);
39
+ baseUrl.searchParams.set('hash', meta.hash);
40
+ return { uri: baseUrl.href, hash };
41
+ }
42
+ if (NativeModules['ExponentKernel']) {
43
+ return {
44
+ uri: `https://classic-assets.eascdn.net/~assets/${encodeURIComponent(hash)}`,
45
+ hash,
46
+ };
47
+ }
48
+ // Locally available on disk (classic-updates embedded asset): no remote URL to build.
49
+ return { uri: '', hash };
50
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getFilename(url: string): string;
2
+ export declare function getFileExtension(url: string): string;
3
+ export declare function getManifestBaseUrl(manifestUrl: string): string;
@@ -0,0 +1,33 @@
1
+ // Ported from expo-asset/src/AssetUris.ts (sdk-57).
2
+ function isDevBuild() {
3
+ return Boolean(globalThis.__DEV__);
4
+ }
5
+ function getBasename(pathname) {
6
+ return pathname.substring(pathname.lastIndexOf('/') + 1);
7
+ }
8
+ export function getFilename(url) {
9
+ const { pathname, searchParams } = new URL(url, 'https://e');
10
+ if (isDevBuild() && searchParams.has('unstable_path')) {
11
+ const encodedFilePath = decodeURIComponent(searchParams.get('unstable_path'));
12
+ return getBasename(encodedFilePath);
13
+ }
14
+ return getBasename(pathname);
15
+ }
16
+ export function getFileExtension(url) {
17
+ const filename = getFilename(url);
18
+ const dotIndex = filename.lastIndexOf('.');
19
+ return dotIndex > 0 ? filename.substring(dotIndex) : '';
20
+ }
21
+ // Rebuilds the URL from parts rather than mutating `protocol`/`pathname` in place: react-native's
22
+ // own global.d.ts types those fields readonly (its URL polyfill's actual shape), conflicting with
23
+ // @types/node's writable declaration wherever both are ambient — same net result either way.
24
+ export function getManifestBaseUrl(manifestUrl) {
25
+ const urlObject = new URL(manifestUrl);
26
+ const protocol = urlObject.protocol === 'exp:'
27
+ ? 'http:'
28
+ : urlObject.protocol === 'exps:'
29
+ ? 'https:'
30
+ : urlObject.protocol;
31
+ const directory = urlObject.pathname.substring(0, urlObject.pathname.lastIndexOf('/') + 1);
32
+ return `${protocol}//${urlObject.host}${directory}`;
33
+ }
@@ -0,0 +1,35 @@
1
+ import { type AssetMetadata } from './asset-sources';
2
+ export declare const ANDROID_EMBEDDED_URL_BASE_RESOURCE = "file:///android_res/";
3
+ export type AssetDescriptor = {
4
+ name: string;
5
+ type: string;
6
+ hash?: string | null;
7
+ uri: string;
8
+ width?: number | null;
9
+ height?: number | null;
10
+ };
11
+ /** A locally- or remotely-sourced asset: metadata plus loading/caching facilities. */
12
+ export declare class Asset {
13
+ private static byHash;
14
+ private static byUri;
15
+ name: string;
16
+ readonly type: string;
17
+ readonly hash: string | null;
18
+ readonly uri: string;
19
+ localUri: string | null;
20
+ width: number | null;
21
+ height: number | null;
22
+ downloaded: boolean;
23
+ private downloading;
24
+ private downloadCallbacks;
25
+ constructor({ name, type, hash, uri, width, height, }: AssetDescriptor);
26
+ static loadAsync(moduleId: number | number[] | string | string[]): Promise<Asset[]>;
27
+ static fromModule(virtualAssetModule: number | string | {
28
+ uri: string;
29
+ width: number;
30
+ height: number;
31
+ }): Asset;
32
+ static fromMetadata(meta: AssetMetadata): Asset;
33
+ static fromURI(uri: string): Asset;
34
+ downloadAsync(): Promise<this>;
35
+ }
@@ -0,0 +1,158 @@
1
+ // Ported from expo-asset/src/Asset.ts (sdk-57), native-only, no Expo Go/expo-updates.
2
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
3
+ /// <reference path="./ts-declarations/react-native-assets.d.ts" />
4
+ import { getAssetByID } from '@react-native/assets-registry/registry';
5
+ import { Platform } from 'expo-modules-core';
6
+ import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
7
+ import { selectAssetSource } from './asset-sources.js';
8
+ import { getFileExtension } from './asset-uris.js';
9
+ import { getLocalAssetUri } from './local-assets.js';
10
+ import { expoAsset } from './native-module.js';
11
+ import { IS_ENV_WITH_LOCAL_ASSETS } from './platform-utils.js';
12
+ export const ANDROID_EMBEDDED_URL_BASE_RESOURCE = 'file:///android_res/';
13
+ /** A locally- or remotely-sourced asset: metadata plus loading/caching facilities. */
14
+ export class Asset {
15
+ static byHash = {};
16
+ static byUri = {};
17
+ name;
18
+ type;
19
+ hash = null;
20
+ uri;
21
+ localUri = null;
22
+ width = null;
23
+ height = null;
24
+ downloaded = false;
25
+ downloading = false;
26
+ downloadCallbacks = [];
27
+ constructor({ name, type, hash = null, uri, width, height, }) {
28
+ this.name = name;
29
+ this.type = type;
30
+ this.hash = hash;
31
+ this.uri = uri;
32
+ if (typeof width === 'number')
33
+ this.width = width;
34
+ if (typeof height === 'number')
35
+ this.height = height;
36
+ if (hash) {
37
+ this.localUri = getLocalAssetUri(hash, type);
38
+ if (this.localUri?.startsWith(ANDROID_EMBEDDED_URL_BASE_RESOURCE)) {
39
+ this.uri = this.localUri;
40
+ this.localUri = null;
41
+ }
42
+ else if (this.localUri) {
43
+ this.downloaded = true;
44
+ }
45
+ }
46
+ }
47
+ static loadAsync(moduleId) {
48
+ const moduleIds = Array.isArray(moduleId) ? moduleId : [moduleId];
49
+ return Promise.all(moduleIds.map(id => Asset.fromModule(id).downloadAsync()));
50
+ }
51
+ static fromModule(virtualAssetModule) {
52
+ if (typeof virtualAssetModule === 'string') {
53
+ return Asset.fromURI(virtualAssetModule);
54
+ }
55
+ if (typeof virtualAssetModule === 'object' &&
56
+ 'uri' in virtualAssetModule &&
57
+ typeof virtualAssetModule.uri === 'string') {
58
+ const extension = getFileExtension(virtualAssetModule.uri);
59
+ return new Asset({
60
+ name: '',
61
+ type: extension.startsWith('.') ? extension.substring(1) : extension,
62
+ hash: null,
63
+ uri: virtualAssetModule.uri,
64
+ width: virtualAssetModule.width,
65
+ height: virtualAssetModule.height,
66
+ });
67
+ }
68
+ const meta = getAssetByID(virtualAssetModule);
69
+ if (!meta) {
70
+ throw new Error(`Module "${virtualAssetModule}" is missing from the asset registry`);
71
+ }
72
+ // Outside a managed (Expo Go / expo-updates) env, the moduleId is needed because
73
+ // resolveAssetSource depends on it — a classic-updates env resolves by hash instead.
74
+ if (!IS_ENV_WITH_LOCAL_ASSETS) {
75
+ const resolved = resolveAssetSource(virtualAssetModule);
76
+ if (!resolved) {
77
+ throw new Error(`Module "${virtualAssetModule}" could not be resolved to an asset source`);
78
+ }
79
+ const asset = new Asset({
80
+ name: meta.name,
81
+ type: meta.type,
82
+ hash: meta.hash,
83
+ uri: resolved.uri,
84
+ width: meta.width,
85
+ height: meta.height,
86
+ });
87
+ // Android drawable resource names (no scheme) are already usable directly, same as RN's
88
+ // own Image component backward-compat handling.
89
+ if (Platform.OS === 'android' &&
90
+ !resolved.uri.includes(':') &&
91
+ (meta.width || meta.height)) {
92
+ asset.localUri = asset.uri;
93
+ asset.downloaded = true;
94
+ }
95
+ Asset.byHash[meta.hash] = asset;
96
+ return asset;
97
+ }
98
+ return Asset.fromMetadata(meta);
99
+ }
100
+ static fromMetadata(meta) {
101
+ const existing = Asset.byHash[meta.hash];
102
+ if (existing)
103
+ return existing;
104
+ const { uri, hash } = selectAssetSource(meta);
105
+ const asset = new Asset({
106
+ name: meta.name,
107
+ type: meta.type,
108
+ hash,
109
+ uri,
110
+ width: meta.width,
111
+ height: meta.height,
112
+ });
113
+ Asset.byHash[meta.hash] = asset;
114
+ return asset;
115
+ }
116
+ static fromURI(uri) {
117
+ const existing = Asset.byUri[uri];
118
+ if (existing)
119
+ return existing;
120
+ let type;
121
+ if (uri.indexOf(';base64') > -1) {
122
+ type = uri.split(';')[0]?.split('/')[1] ?? '';
123
+ }
124
+ else {
125
+ const extension = getFileExtension(uri);
126
+ type = extension.startsWith('.') ? extension.substring(1) : extension;
127
+ }
128
+ const asset = new Asset({ name: '', type, hash: null, uri });
129
+ Asset.byUri[uri] = asset;
130
+ return asset;
131
+ }
132
+ async downloadAsync() {
133
+ if (this.downloaded)
134
+ return this;
135
+ if (this.downloading) {
136
+ await new Promise((resolve, reject) => {
137
+ this.downloadCallbacks.push({ resolve, reject });
138
+ });
139
+ return this;
140
+ }
141
+ this.downloading = true;
142
+ try {
143
+ this.localUri = await expoAsset.downloadAsync(this.uri, this.hash, this.type);
144
+ this.downloaded = true;
145
+ this.downloadCallbacks.forEach(({ resolve }) => resolve());
146
+ }
147
+ catch (error) {
148
+ const reason = error instanceof Error ? error : new Error(String(error));
149
+ this.downloadCallbacks.forEach(({ reject }) => reject(reason));
150
+ throw reason;
151
+ }
152
+ finally {
153
+ this.downloading = false;
154
+ this.downloadCallbacks = [];
155
+ }
156
+ return this;
157
+ }
158
+ }
@@ -0,0 +1,3 @@
1
+ import './asset-fx';
2
+ export { Asset, ANDROID_EMBEDDED_URL_BASE_RESOURCE, type AssetDescriptor, } from './asset';
3
+ export type { AssetMetadata, AssetSource } from './asset-sources';
@@ -0,0 +1,2 @@
1
+ import './asset-fx.js';
2
+ export { Asset, ANDROID_EMBEDDED_URL_BASE_RESOURCE, } from './asset.js';
@@ -0,0 +1 @@
1
+ export declare function getLocalAssetUri(hash: string, type: string | null): string | null;
@@ -0,0 +1,16 @@
1
+ // Ported verbatim from expo-asset/src/LocalAssets.ts (sdk-57). localAssets comes from
2
+ // expo-updates (classic-updates/Expo-Go asset overrides) via platform-utils.ts — always `{}`
3
+ // in a bare app that ships neither, so this always falls through to null there.
4
+ import { getLocalAssets } from './platform-utils.js';
5
+ const localAssets = getLocalAssets();
6
+ export function getLocalAssetUri(hash, type) {
7
+ const localAssetsKey = hash;
8
+ const legacyLocalAssetsKey = `${hash}.${type ?? ''}`;
9
+ if (localAssetsKey in localAssets) {
10
+ return localAssets[localAssetsKey] ?? null;
11
+ }
12
+ if (legacyLocalAssetsKey in localAssets) {
13
+ return localAssets[legacyLocalAssetsKey] ?? null;
14
+ }
15
+ return null;
16
+ }
@@ -0,0 +1,4 @@
1
+ export type INativeAssetModule = {
2
+ downloadAsync(url: string, md5Hash: string | null, type: string): Promise<string>;
3
+ };
4
+ export declare const expoAsset: INativeAssetModule;