# Asset

> Resolve a bundled require() asset or a URI to a local file on every SymbioteNative adapter.

Turn `require('./logo.png')` (or a plain URI) into a file you can hand to anything that wants a
local path: download it once, read `localUri`, reuse it. `@symbiote-native/asset` wraps
[`expo-asset`](https://github.com/expo/expo/tree/main/packages/expo-asset) so every SymbioteNative
adapter can do it, not just React. It exists mainly so
[`@symbiote-native/font`](/docs/packages/font/) can accept the same `number` and `Asset` font
sources as upstream `expo-font`, and works standalone too.

| OS platform | Support |
| ----------- | ------- |
| iOS         | live    |
| Android     | live    |

| Framework adapter | Support |
| ----------------- | ------- |
| React             | live    |
| Vue               | live    |
| Angular           | live    |
| Svelte            | live    |
| Solid             | live    |

## Installation

```sh
npm install @symbiote-native/asset
```

Scaffolding or extending a SymbioteNative app? `npx @symbiote-native/cli new --asset` (or
`add --asset` in an existing app) installs and wires this for you - see
[`@symbiote-native/cli`](https://github.com/OneEyed1366/symbiote-native/tree/master/packages/cli).

`expo-asset` and `expo-modules-core` come along as regular dependencies, pinned to exact versions.
Never install either yourself, and never add the `expo` meta-package to your project (it bundles
its own Metro/Babel pipeline, which conflicts with this project's own).

<Aside type="danger" title="Native setup is required before first use">
  `expo-asset`'s native code is discovered by `expo-modules-autolinking`, a different mechanism from
  the `react-native.config.cjs`/podspec autolinking every other SymbioteNative wrapper uses. Follow
  [How to: wire up an Expo native module](/docs/howtos/expo-native-module-setup/) once per app
  first; it covers this package and every other `expo-modules-core` package with zero further
  native changes.
</Aside>

## Usage

Each adapter wraps `Asset.loadAsync` and loads once on mount, matching upstream's `useAssets`
contract. `assets` stays empty until the download finishes.

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    import { useAssets } from '@symbiote-native/asset/react';

    export function Logo() {
      const [assets, error] = useAssets(require('./assets/logo.png'));
      return assets ? <image source={{ uri: assets[0].localUri ?? undefined }} /> : null;
    }
    ```

  </TabItem>
  <TabItem label="Vue">
    ```vue
    <script setup lang="ts">
    import { useAssets } from '@symbiote-native/asset/vue';

    const { assets, error } = useAssets(require('./assets/logo.png'));
    </script>

    <template>
      <image v-if="assets" :source="{ uri: assets[0].localUri ?? undefined }" />
    </template>
    ```

  </TabItem>
  <TabItem label="Angular">
    ```ts
    import { Component, inject } from '@angular/core';
    import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
    import { AssetsService } from '@symbiote-native/asset/angular';

    @Component({
      standalone: true,
      imports: [SYMBIOTE_ELEMENTS],
      template: `@if (assets.assets(); as loaded) {
        <image [source]="{ uri: loaded[0].localUri ?? undefined }" />
      }`,
    })
    export class Logo {
      readonly assets = inject(AssetsService).connect(require('./assets/logo.png'));
    }
    ```

    `connect()` returns a pair of signals (`assets` and `error`).

  </TabItem>
  <TabItem label="Svelte">
    ```svelte
    <script lang="ts">
      import { useAssets } from '@symbiote-native/asset/svelte';

      const { assets, error } = useAssets(require('./assets/logo.png'));
    </script>

    {#if assets}
      <image source={{ uri: assets[0].localUri ?? undefined }} />
    {/if}
    ```

  </TabItem>
  <TabItem label="Solid">
    ```tsx
    import { createAssets } from '@symbiote-native/asset/solid';

    export function Logo() {
      const { assets } = createAssets(require('./assets/logo.png'));

      return assets() ? (
        <image source={{ uri: assets()?.[0]?.localUri ?? undefined }} />
      ) : null;
    }
    ```

    Solid reserves `use*` for consuming existing state, so the primitive is `createAssets`. Call
    the accessor (`assets()`): a Solid component body runs once.

  </TabItem>
</Tabs>

Outside a component, use the `Asset` class directly:

```ts
import { Asset } from '@symbiote-native/asset';

const [logo] = await Asset.loadAsync(require('./assets/logo.png'));
console.log(logo.localUri); // file:// path in the cache
```

## API

### `Asset`

| Signature                                                | Description                                                                                         |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `Asset.fromModule(moduleIdOrUriOrSource): Asset`         | Builds an `Asset` from a `require()` module id, a URI, or an object with `uri`, `width`, `height`   |
| `Asset.fromMetadata(meta): Asset`                        | Builds an `Asset` from bundler metadata (`AssetMetadata`)                                           |
| `Asset.fromURI(uri): Asset`                              | Builds an `Asset` for a plain URI, without downloading it                                           |
| `Asset.loadAsync(moduleId): Promise<Asset[]>`            | Resolves one or many module ids (or URIs) and downloads each. Resolves the assets, in order         |
| `asset.downloadAsync(): Promise<Asset>`                  | Downloads this asset to a local cache file and sets `localUri`. Resolves the same asset             |

### `useAssets` / `createAssets` / `AssetsService`

| Adapter | Entry point                              | Description                                                      |
| ------- | ---------------------------------------- | ---------------------------------------------------------------- |
| React   | `useAssets(moduleIds)`                   | Returns `[assets: Asset[] \| undefined, error: Error \| undefined]` |
| Vue     | `useAssets(moduleIds)`                   | Returns `{ assets, error }` as refs                              |
| Svelte  | `useAssets(moduleIds)`                   | Returns an object with reactive `assets` and `error`             |
| Solid   | `createAssets(moduleIds)`                | Returns `{ assets(), error() }` as accessors                     |
| Angular | `inject(AssetsService).connect(moduleIds)` | Returns `{ assets, error }` as signals                         |

The hook entry points accept `number | number[]` only, matching upstream's own hook (narrower than
`Asset.loadAsync`, which also takes strings).

## Notes

- **`localUri` is `null` until the asset is downloaded.** Read it after `loadAsync` or
  `downloadAsync` resolves, and pass `?? undefined` where an image source expects `string | undefined`.
- **`useAssets` does not reload.** Like upstream, it loads once on mount; passing a different module
  id later has no effect.
- **Expo Go and `expo-updates` code paths are ported but inert.** `getLocalAssetUri`, the manifest2
  dev-server resolution and the `Image` source transformer are real, tested code. This repo ships
  neither Expo Go nor `expo-updates`, so `IS_ENV_WITH_LOCAL_ASSETS` is `false`. If your app installs
  `expo-updates` itself, this package picks it up, same as upstream.
- **The `expo-asset` config plugin is not ported.** It links files into the native project at
  build time via `app.json`; this repo runs no Expo prebuild step. Bundle files through Metro with
  `require()`, and add unusual extensions (such as `.lottie`) to `assetExts` in your Metro config.
- **The web platform is not ported.** Only the native path exists, as with every package here.

## Common questions

**`asset.localUri` is `null`.** It stays `null` until the asset is downloaded. Read it after
`Asset.loadAsync(...)` or `asset.downloadAsync()` resolves, or let `useAssets` do the wait for you.

**How do I preload several assets before showing a screen?** Pass them all to one call:
`await Asset.loadAsync([require('./a.png'), require('./b.png')])`. It resolves the assets in order.

**Where does the downloaded file go?** A file in the app's cache directory, so the OS may remove it
later. Do not store anything there you cannot re-create.

**Does the hook reload when I change the module id?** No. It loads once on mount, like upstream.
Mount a new component (or use a key) for a different asset.

Sources: [Expo docs: Asset](https://docs.expo.dev/versions/latest/sdk/asset/),
[expo/expo#8222 downloadAsync should not download if file exists](https://github.com/expo/expo/pull/8222).

## How the wrapper works

`expo-asset`'s JS is hand-ported into this package's `core/`, resolving `ExpoAsset` through
`expo-modules-core` rather than the `expo` meta-package:

```
packages/asset/src/
|-- core/     # asset.ts, asset-sources.ts, asset-uris.ts, local-assets.ts, platform-utils.ts
|-- react/    # hooks/        useAssets
|-- vue/      # composables/  useAssets
|-- svelte/   # runes/        useAssets
|-- solid/    # primitives/   createAssets
`-- angular/  # services/     AssetsService
```

Each adapter file is lifecycle only. The native code is never vendored: `expo-modules-autolinking`
resolves it from `node_modules` (see [the native setup guide](/docs/howtos/expo-native-module-setup/)).
