@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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +4 -0
- package/build/angular/services/assets.service/index.d.ts +8 -0
- package/build/angular/services/assets.service/index.js +64 -0
- package/build/core/asset-fx.d.ts +1 -0
- package/build/core/asset-fx.js +24 -0
- package/build/core/asset-sources.d.ts +13 -0
- package/build/core/asset-sources.js +50 -0
- package/build/core/asset-uris.d.ts +3 -0
- package/build/core/asset-uris.js +33 -0
- package/build/core/asset.d.ts +35 -0
- package/build/core/asset.js +158 -0
- package/build/core/index.d.ts +3 -0
- package/build/core/index.js +2 -0
- package/build/core/local-assets.d.ts +1 -0
- package/build/core/local-assets.js +16 -0
- package/build/core/native-module.d.ts +4 -0
- package/build/core/native-module.js +2 -0
- package/build/core/platform-utils.d.ts +5 -0
- package/build/core/platform-utils.js +28 -0
- package/build/react/hooks/use-assets/index.d.ts +2 -0
- package/build/react/hooks/use-assets/index.js +14 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +3 -0
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +5 -0
- package/build/solid/primitives/create-assets.d.ts +6 -0
- package/build/solid/primitives/create-assets.js +10 -0
- package/build/svelte/index.d.ts +2 -0
- package/build/svelte/index.js +5 -0
- package/build/svelte/runes/use-assets.svelte.d.ts +5 -0
- package/build/svelte/runes/use-assets.svelte.js +24 -0
- package/build/vue/composables/use-assets/index.d.ts +6 -0
- package/build/vue/composables/use-assets/index.js +18 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +3 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/assets.service/index.d.ts +11 -0
- package/build-ngc/angular/services/assets.service/index.js +24 -0
- package/build-ngc/angular/services/assets.service/index.js.map +1 -0
- package/build-ngc/core/asset-fx.d.ts +1 -0
- package/build-ngc/core/asset-fx.js +25 -0
- package/build-ngc/core/asset-fx.js.map +1 -0
- package/build-ngc/core/asset-sources.d.ts +13 -0
- package/build-ngc/core/asset-sources.js +51 -0
- package/build-ngc/core/asset-sources.js.map +1 -0
- package/build-ngc/core/asset-uris.d.ts +3 -0
- package/build-ngc/core/asset-uris.js +34 -0
- package/build-ngc/core/asset-uris.js.map +1 -0
- package/build-ngc/core/asset.d.ts +35 -0
- package/build-ngc/core/asset.js +159 -0
- package/build-ngc/core/asset.js.map +1 -0
- package/build-ngc/core/index.d.ts +3 -0
- package/build-ngc/core/index.js +3 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/local-assets.d.ts +1 -0
- package/build-ngc/core/local-assets.js +17 -0
- package/build-ngc/core/local-assets.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +4 -0
- package/build-ngc/core/native-module.js +3 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/platform-utils.d.ts +5 -0
- package/build-ngc/core/platform-utils.js +29 -0
- package/build-ngc/core/platform-utils.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +151 -0
- package/src/angular/index.ts +4 -0
- package/src/angular/services/assets.service/index.ts +21 -0
- package/src/core/asset-fx.ts +30 -0
- package/src/core/asset-sources.ts +80 -0
- package/src/core/asset-uris.ts +51 -0
- package/src/core/asset.ts +212 -0
- package/src/core/index.ts +8 -0
- package/src/core/local-assets.ts +22 -0
- package/src/core/native-module.ts +11 -0
- package/src/core/platform-utils.ts +47 -0
- package/src/core/ts-declarations/react-native-assets.d.ts +75 -0
- package/src/react/hooks/use-assets/index.ts +19 -0
- package/src/react/index.ts +4 -0
- package/src/solid/index.ts +6 -0
- package/src/solid/primitives/create-assets.ts +16 -0
- package/src/svelte/index.ts +6 -0
- package/src/svelte/runes/use-assets.svelte.ts +30 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/vue/composables/use-assets/index.ts +30 -0
- 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,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,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 @@
|
|
|
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
|
+
}
|