diff --git a/example/assets/rive/GradientBorder.riv.d.ts b/example/assets/rive/GradientBorder.riv.d.ts
index 22cc7616..f9af2963 100644
--- a/example/assets/rive/GradientBorder.riv.d.ts
+++ b/example/assets/rive/GradientBorder.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
isFocused: 'boolean';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/arbtboards-models-instances.riv.d.ts b/example/assets/rive/arbtboards-models-instances.riv.d.ts
index 0f4d9df9..b269f181 100644
--- a/example/assets/rive/arbtboards-models-instances.riv.d.ts
+++ b/example/assets/rive/arbtboards-models-instances.riv.d.ts
@@ -22,5 +22,6 @@ declare const asset: RiveAsset<{
_id: 'string';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/artboard_db_test.riv.d.ts b/example/assets/rive/artboard_db_test.riv.d.ts
index d3e059d3..fe9280e6 100644
--- a/example/assets/rive/artboard_db_test.riv.d.ts
+++ b/example/assets/rive/artboard_db_test.riv.d.ts
@@ -17,5 +17,6 @@ declare const asset: RiveAsset<{
artboard_2: 'artboard';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/blinko.riv.d.ts b/example/assets/rive/blinko.riv.d.ts
index eeb44052..2f187e67 100644
--- a/example/assets/rive/blinko.riv.d.ts
+++ b/example/assets/rive/blinko.riv.d.ts
@@ -93,5 +93,6 @@ declare const asset: RiveAsset<{
'property of gameLogicVM': 'viewModel:gameLogicVM';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/bouncing_ball.riv.d.ts b/example/assets/rive/bouncing_ball.riv.d.ts
index 80f4b9ed..3338834b 100644
--- a/example/assets/rive/bouncing_ball.riv.d.ts
+++ b/example/assets/rive/bouncing_ball.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
ypos: 'number';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/click-count.riv.d.ts b/example/assets/rive/click-count.riv.d.ts
index d30dbe90..1a3d37cf 100644
--- a/example/assets/rive/click-count.riv.d.ts
+++ b/example/assets/rive/click-count.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
clickCount: 'number';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/counter.riv.d.ts b/example/assets/rive/counter.riv.d.ts
index 85084ab5..48e231c9 100644
--- a/example/assets/rive/counter.riv.d.ts
+++ b/example/assets/rive/counter.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
cnt: 'number';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/databinding.riv.d.ts b/example/assets/rive/databinding.riv.d.ts
index 6f919f93..590807a6 100644
--- a/example/assets/rive/databinding.riv.d.ts
+++ b/example/assets/rive/databinding.riv.d.ts
@@ -24,5 +24,6 @@ declare const asset: RiveAsset<{
name: 'string';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/databinding_images.riv.d.ts b/example/assets/rive/databinding_images.riv.d.ts
index 22274caa..2a0ec197 100644
--- a/example/assets/rive/databinding_images.riv.d.ts
+++ b/example/assets/rive/databinding_images.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
bound_image: 'image';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/databinding_lists.riv.d.ts b/example/assets/rive/databinding_lists.riv.d.ts
index 1db323e2..125a97c6 100644
--- a/example/assets/rive/databinding_lists.riv.d.ts
+++ b/example/assets/rive/databinding_lists.riv.d.ts
@@ -16,5 +16,6 @@ declare const asset: RiveAsset<{
team: 'list';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/fallback_fonts.riv.d.ts b/example/assets/rive/fallback_fonts.riv.d.ts
index 30de9582..67b1bea3 100644
--- a/example/assets/rive/fallback_fonts.riv.d.ts
+++ b/example/assets/rive/fallback_fonts.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
Artboard: 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/font_fallback.riv.d.ts b/example/assets/rive/font_fallback.riv.d.ts
index 62e4a100..aa912107 100644
--- a/example/assets/rive/font_fallback.riv.d.ts
+++ b/example/assets/rive/font_fallback.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
text: 'string';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/hello_world_text.riv.d.ts b/example/assets/rive/hello_world_text.riv.d.ts
index 724081b0..532e6118 100644
--- a/example/assets/rive/hello_world_text.riv.d.ts
+++ b/example/assets/rive/hello_world_text.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
'New Artboard': 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/inputglow.riv.d.ts b/example/assets/rive/inputglow.riv.d.ts
index 482c9c41..1ad6a2aa 100644
--- a/example/assets/rive/inputglow.riv.d.ts
+++ b/example/assets/rive/inputglow.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
isFocused: 'boolean';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/ios_android_layouts_demo_v01.riv.d.ts b/example/assets/rive/ios_android_layouts_demo_v01.riv.d.ts
index 9b0ea4d7..1843cab1 100644
--- a/example/assets/rive/ios_android_layouts_demo_v01.riv.d.ts
+++ b/example/assets/rive/ios_android_layouts_demo_v01.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
iOS_Android_Layouts_demo_v01: 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/layout_test.riv.d.ts b/example/assets/rive/layout_test.riv.d.ts
index 99c1eb8f..79c4febd 100644
--- a/example/assets/rive/layout_test.riv.d.ts
+++ b/example/assets/rive/layout_test.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/layouts_demo.riv.d.ts b/example/assets/rive/layouts_demo.riv.d.ts
index 037effe5..58acaaf9 100644
--- a/example/assets/rive/layouts_demo.riv.d.ts
+++ b/example/assets/rive/layouts_demo.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
iOS_Android_Layouts_demo_v01: 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/many_viewmodels.riv.d.ts b/example/assets/rive/many_viewmodels.riv.d.ts
index f1abe2fb..68f65647 100644
--- a/example/assets/rive/many_viewmodels.riv.d.ts
+++ b/example/assets/rive/many_viewmodels.riv.d.ts
@@ -15,5 +15,6 @@ declare const asset: RiveAsset<{
textValue: 'string';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/movecircle.riv.d.ts b/example/assets/rive/movecircle.riv.d.ts
index 34cb1c49..571437fd 100644
--- a/example/assets/rive/movecircle.riv.d.ts
+++ b/example/assets/rive/movecircle.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
posY: 'number';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/nodefaultbouncing.riv.d.ts b/example/assets/rive/nodefaultbouncing.riv.d.ts
index b9abfda0..5eb216bc 100644
--- a/example/assets/rive/nodefaultbouncing.riv.d.ts
+++ b/example/assets/rive/nodefaultbouncing.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/on_entry_test.riv.d.ts b/example/assets/rive/on_entry_test.riv.d.ts
index b50e3df2..1227bbc3 100644
--- a/example/assets/rive/on_entry_test.riv.d.ts
+++ b/example/assets/rive/on_entry_test.riv.d.ts
@@ -14,5 +14,8 @@ declare const asset: RiveAsset<{
inputOnEntry: 'number';
};
};
+ assets: {
+ 'Inter-4228759': 'font';
+ };
}>;
export default asset;
diff --git a/example/assets/rive/out_of_band.riv.d.ts b/example/assets/rive/out_of_band.riv.d.ts
index aa987dcc..0bfc7eeb 100644
--- a/example/assets/rive/out_of_band.riv.d.ts
+++ b/example/assets/rive/out_of_band.riv.d.ts
@@ -9,5 +9,13 @@ declare const asset: RiveAsset<{
Artboard: 'State Machine 1';
};
viewModels: {};
+ assets: {
+ 'Inter-594377': 'font';
+ 'referenced-image-2929282': 'image';
+ 'referenced_audio-2929340': 'audio';
+ 'hosted_audio-2989208': 'audio';
+ 'Damion-593587': 'font';
+ 'cdn-image-2989123': 'image';
+ };
}>;
export default asset;
diff --git a/example/assets/rive/quick_start.riv.d.ts b/example/assets/rive/quick_start.riv.d.ts
index 057517fe..bfcc67f1 100644
--- a/example/assets/rive/quick_start.riv.d.ts
+++ b/example/assets/rive/quick_start.riv.d.ts
@@ -17,5 +17,6 @@ declare const asset: RiveAsset<{
health: 'number';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/rating.riv.d.ts b/example/assets/rive/rating.riv.d.ts
index c234d3af..1c14f47f 100644
--- a/example/assets/rive/rating.riv.d.ts
+++ b/example/assets/rive/rating.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
'New Artboard': 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/rewards.riv.d.ts b/example/assets/rive/rewards.riv.d.ts
index 68adbe4f..b838e3ce 100644
--- a/example/assets/rive/rewards.riv.d.ts
+++ b/example/assets/rive/rewards.riv.d.ts
@@ -47,5 +47,6 @@ declare const asset: RiveAsset<{
Item_Selection: 'enum:Coin|Gem';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/style_fallback_fonts.riv.d.ts b/example/assets/rive/style_fallback_fonts.riv.d.ts
index bc1a66d1..3ad8a247 100644
--- a/example/assets/rive/style_fallback_fonts.riv.d.ts
+++ b/example/assets/rive/style_fallback_fonts.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
Artboard: 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/tabtest.riv.d.ts b/example/assets/rive/tabtest.riv.d.ts
index cc6448b3..13847740 100644
--- a/example/assets/rive/tabtest.riv.d.ts
+++ b/example/assets/rive/tabtest.riv.d.ts
@@ -13,5 +13,6 @@ declare const asset: RiveAsset<{
enumProperty: 'enum:All|Parent|Child';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/viewmodelproperty.riv.d.ts b/example/assets/rive/viewmodelproperty.riv.d.ts
index 23cd8185..7aac63b8 100644
--- a/example/assets/rive/viewmodelproperty.riv.d.ts
+++ b/example/assets/rive/viewmodelproperty.riv.d.ts
@@ -17,5 +17,6 @@ declare const asset: RiveAsset<{
name: 'string';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/assets/rive/vm_value_change_test.riv.d.ts b/example/assets/rive/vm_value_change_test.riv.d.ts
index b5298568..765be932 100644
--- a/example/assets/rive/vm_value_change_test.riv.d.ts
+++ b/example/assets/rive/vm_value_change_test.riv.d.ts
@@ -14,5 +14,6 @@ declare const asset: RiveAsset<{
textValue: 'string';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/example/src/exercisers/OutOfBandAssets.tsx b/example/src/exercisers/OutOfBandAssets.tsx
index 0ad9d93d..8b72eaea 100644
--- a/example/src/exercisers/OutOfBandAssets.tsx
+++ b/example/src/exercisers/OutOfBandAssets.tsx
@@ -13,30 +13,30 @@ import {
} from '@rive-app/react-native';
import { Picker } from '@react-native-picker/picker';
import { type Metadata } from '../shared/metadata';
+// Typed via the generated out_of_band.riv.d.ts — the referencedAssets keys
+// and each entry's `type` below are checked against the file's actual assets.
+import outOfBandRiv from '../../assets/rive/out_of_band.riv';
export default function OutOfBandAssetsExample() {
const [uri, setUri] = React.useState('https://picsum.photos/id/372/500/500');
- const { riveFile, isLoading, error } = useRiveFile(
- require('../../assets/rive/out_of_band.riv'),
- {
- referencedAssets: {
- 'Inter-594377': {
- source: require('../../assets/fonts/Inter-594377.ttf'),
- type: 'font',
- },
- 'referenced-image-2929282': {
- source: {
- uri: uri,
- },
- type: 'image',
- },
- 'referenced_audio-2929340': {
- source: require('../../assets/audio/referenced_audio-2929340.wav'),
- type: 'audio',
+ const { riveFile, isLoading, error } = useRiveFile(outOfBandRiv, {
+ referencedAssets: {
+ 'Inter-594377': {
+ source: require('../../assets/fonts/Inter-594377.ttf'),
+ type: 'font',
+ },
+ 'referenced-image-2929282': {
+ source: {
+ uri: uri,
},
+ type: 'image',
},
- }
- );
+ 'referenced_audio-2929340': {
+ source: require('../../assets/audio/referenced_audio-2929340.wav'),
+ type: 'audio',
+ },
+ },
+ });
if (isLoading) {
return ;
diff --git a/expo-example/assets/rive/rating.riv.d.ts b/expo-example/assets/rive/rating.riv.d.ts
index c234d3af..1c14f47f 100644
--- a/expo-example/assets/rive/rating.riv.d.ts
+++ b/expo-example/assets/rive/rating.riv.d.ts
@@ -9,5 +9,6 @@ declare const asset: RiveAsset<{
'New Artboard': 'State Machine 1';
};
viewModels: {};
+ assets: {};
}>;
export default asset;
diff --git a/expo-example/assets/rive/rewards_source.riv.d.ts b/expo-example/assets/rive/rewards_source.riv.d.ts
index 0dccac86..bb81c624 100644
--- a/expo-example/assets/rive/rewards_source.riv.d.ts
+++ b/expo-example/assets/rive/rewards_source.riv.d.ts
@@ -47,5 +47,6 @@ declare const asset: RiveAsset<{
Item_Selection: 'enum:Coin|Gem';
};
};
+ assets: {};
}>;
export default asset;
diff --git a/scripts/__tests__/rive-gen-emit.test.ts b/scripts/__tests__/rive-gen-emit.test.ts
index dc384466..ba4fb25c 100644
--- a/scripts/__tests__/rive-gen-emit.test.ts
+++ b/scripts/__tests__/rive-gen-emit.test.ts
@@ -6,6 +6,8 @@ import {
smRecord,
vmRecord,
schemaBody,
+ assetsRecord,
+ classifyAsset,
enumTypeString,
viewModelRefTypeString,
type Schema,
@@ -47,6 +49,7 @@ describe('emit escaping', () => {
viewModels: {
"It's VM": { "quote'": "enum:a'b" },
},
+ assets: { "Font's-123": 'font' },
};
const body = schemaBody(schema);
expect(parseErrors(`declare const asset: {\n${body}\n};`)).toEqual([]);
@@ -78,10 +81,21 @@ describe('schemaBody', () => {
defaultArtboard: 'Main',
stateMachines: { Main: ['SM'] },
viewModels: {},
+ assets: {},
};
- test('always emits viewModels, empty object when none', () => {
+ test('always emits viewModels and assets, empty objects when none', () => {
expect(schemaBody(base)).toContain('viewModels: {};');
+ expect(schemaBody(base)).toContain('assets: {};');
+ });
+
+ test('emits assets record when present', () => {
+ const body = schemaBody({
+ ...base,
+ assets: { 'Inter-594377': 'font', 'img-1': 'image' },
+ });
+ expect(body).toContain("'Inter-594377': 'font';");
+ expect(body).toContain("'img-1': 'image';");
});
test('emits viewModels record when present', () => {
@@ -137,3 +151,52 @@ describe('viewModelRefTypeString', () => {
expect(viewModelRefTypeString(inst, 'Coin')).toBe('viewModel');
});
});
+
+describe('classifyAsset', () => {
+ const font = {
+ name: 'Inter',
+ uniqueFilename: 'Inter-594377.ttf',
+ fileExtension: 'ttf',
+ isFont: true,
+ };
+
+ test('referenced asset → unique id (extension stripped) + kind', () => {
+ expect(classifyAsset(font, 0)).toEqual({
+ id: 'Inter-594377',
+ kind: 'font',
+ });
+ expect(
+ classifyAsset(
+ { uniqueFilename: 'pic-1.png', fileExtension: 'png', isImage: true },
+ 0
+ )
+ ).toEqual({ id: 'pic-1', kind: 'image' });
+ expect(
+ classifyAsset(
+ { uniqueFilename: 'a-2.wav', fileExtension: 'wav', isAudio: true },
+ 0
+ )
+ ).toEqual({ id: 'a-2', kind: 'audio' });
+ });
+
+ test('embedded assets are excluded', () => {
+ expect(classifyAsset(font, 8680)).toBeNull();
+ });
+
+ test('unknown asset kinds are excluded', () => {
+ expect(classifyAsset({ uniqueFilename: 'x-1.bin' }, 0)).toBeNull();
+ });
+
+ test('falls back to name when uniqueFilename is missing', () => {
+ expect(classifyAsset({ name: 'Inter', isFont: true }, 0)).toEqual({
+ id: 'Inter',
+ kind: 'font',
+ });
+ });
+});
+
+describe('assetsRecord', () => {
+ test('escapes hostile identifiers', () => {
+ expect(assetsRecord({ "It's-1": 'font' })).toBe(" 'It\\'s-1': 'font';");
+ });
+});
diff --git a/scripts/__tests__/rive-gen-fuzz.test.ts b/scripts/__tests__/rive-gen-fuzz.test.ts
index 61fd238d..6f5bf4bc 100644
Binary files a/scripts/__tests__/rive-gen-fuzz.test.ts and b/scripts/__tests__/rive-gen-fuzz.test.ts differ
diff --git a/scripts/rive-extract-schema.ts b/scripts/rive-extract-schema.ts
index c91b04ab..9c0b3066 100644
--- a/scripts/rive-extract-schema.ts
+++ b/scripts/rive-extract-schema.ts
@@ -13,7 +13,11 @@ import { readFileSync } from 'fs';
// loader cannot statically see its named exports (bun's interop can).
import riveCanvas from '@rive-app/canvas';
const { RuntimeLoader } = riveCanvas;
-import { enumTypeString, viewModelRefTypeString } from './rive-gen-types.ts';
+import {
+ classifyAsset,
+ enumTypeString,
+ viewModelRefTypeString,
+} from './rive-gen-types.ts';
// noUncheckedIndexedAccess: process.argv destructuring yields string | undefined
const input: string | undefined = process.argv[2];
@@ -67,8 +71,13 @@ async function main() {
// names/schemas — decoding (images especially) goes through render paths
// that stall load() forever without WebGL; a pending load() then drains the
// event loop and the process exits 0 without output.
+ const assets: Record = {};
const assetLoader = new (runtime as any).CustomFileAssetLoader({
- loadContents: () => true,
+ loadContents: (asset: any, embeddedBytes: Uint8Array | undefined) => {
+ const classified = classifyAsset(asset ?? {}, embeddedBytes?.length ?? 0);
+ if (classified) assets[classified.id] = classified.kind;
+ return true;
+ },
});
let timer: ReturnType | undefined;
@@ -130,7 +139,7 @@ async function main() {
const defaultArtboard = artboards[0] ?? '';
process.stdout.write(
JSON.stringify(
- { artboards, defaultArtboard, stateMachines, viewModels },
+ { artboards, defaultArtboard, stateMachines, viewModels, assets },
null,
2
) + '\n'
diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts
index 811ae22e..557315d3 100644
--- a/scripts/rive-gen-types.ts
+++ b/scripts/rive-gen-types.ts
@@ -51,6 +51,45 @@ export interface Schema {
defaultArtboard: string;
stateMachines: Record;
viewModels: Record>;
+ /** Non-embedded (referenced or CDN) assets: unique identifier → kind. */
+ assets: Record;
+}
+
+/**
+ * Classify a file asset reported by the WASM asset loader. Only non-embedded
+ * assets (referenced or CDN-hosted) are schema-relevant: they are the ones an
+ * app must supply via `referencedAssets` — the new runtime does not fetch CDN
+ * assets itself. The key is the unique identifier (uniqueFilename minus
+ * extension, e.g. 'Inter-594377'), the form the runtime documents as the
+ * recommended `referencedAssets` key.
+ */
+export function classifyAsset(
+ asset: {
+ name?: string;
+ uniqueFilename?: string;
+ fileExtension?: string;
+ isImage?: boolean;
+ isFont?: boolean;
+ isAudio?: boolean;
+ },
+ embeddedByteCount: number
+): { id: string; kind: string } | null {
+ if (embeddedByteCount > 0) return null;
+ const kind = asset.isImage
+ ? 'image'
+ : asset.isFont
+ ? 'font'
+ : asset.isAudio
+ ? 'audio'
+ : null;
+ if (!kind) return null;
+ const unique = asset.uniqueFilename ?? '';
+ const ext = asset.fileExtension ?? '';
+ const id =
+ ext && unique.endsWith(`.${ext}`)
+ ? unique.slice(0, -(ext.length + 1))
+ : unique || (asset.name ?? '');
+ return id ? { id, kind } : null;
}
let runtimeReady: Promise | null = null;
@@ -81,8 +120,15 @@ async function extractSchema(input: string): Promise {
// names/schemas — decoding (images especially) goes through render paths
// that stall load() forever without WebGL, silently truncating the batch:
// a pending load() drains bun's event loop and the process exits 0.
+ // The load() callbacks are also the only place asset metadata is visible,
+ // so record it here.
+ const assets: Record = {};
const assetLoader = new (runtime as any).CustomFileAssetLoader({
- loadContents: () => true,
+ loadContents: (asset: any, embeddedBytes: Uint8Array | undefined) => {
+ const classified = classifyAsset(asset ?? {}, embeddedBytes?.length ?? 0);
+ if (classified) assets[classified.id] = classified.kind;
+ return true;
+ },
});
let timer: ReturnType | undefined;
@@ -145,6 +191,7 @@ async function extractSchema(input: string): Promise {
defaultArtboard: artboards[0] ?? '',
stateMachines,
viewModels,
+ assets,
};
}
@@ -191,19 +238,31 @@ export function vmRecord(
.join('\n');
}
+export function assetsRecord(assets: Record): string {
+ const keys = Object.keys(assets);
+ const force = keys.some(needsQuote);
+ return Object.entries(assets)
+ .map(([id, kind]) => ` ${quoteKey(id, force)}: ${strLit(kind)};`)
+ .join('\n');
+}
+
export function schemaBody(schema: Schema): string {
- // Always emit viewModels — omitting it would fail the RiveFileSchema
- // constraint and silently degrade the whole asset to untyped.
+ // Always emit viewModels/assets — omitting either would fail the
+ // RiveFileSchema constraint and silently degrade the whole asset to untyped.
const vmSection =
Object.keys(schema.viewModels).length > 0
? `\n viewModels: {\n${vmRecord(schema.viewModels)}\n };`
: '\n viewModels: {};';
+ const assetSection =
+ Object.keys(schema.assets).length > 0
+ ? `\n assets: {\n${assetsRecord(schema.assets)}\n };`
+ : '\n assets: {};';
return `\
artboards: ${schema.artboards.map(strLit).join(' | ')};
defaultArtboard: ${strLit(schema.defaultArtboard)};
stateMachines: {
${smRecord(schema.stateMachines)}
- };${vmSection}`;
+ };${vmSection}${assetSection}`;
}
function dtsContent(input: string, schema: Schema): string {
diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts
index 47510bef..d15c1d1f 100644
--- a/src/__tests__/typed-rive.test-d.ts
+++ b/src/__tests__/typed-rive.test-d.ts
@@ -24,7 +24,10 @@ import { useRiveNumber } from '../../src/hooks/useRiveNumber';
import { useRiveEnum } from '../../src/hooks/useRiveEnum';
import { useViewModelInstance } from '../../src/hooks/useViewModelInstance';
import { RiveView, type RiveViewProps } from '../../src/core/RiveView';
+import { useRiveFile } from '../../src/hooks/useRiveFile';
+import type { RiveImage } from '../../src/specs/RiveImage.nitro';
import gradientBorderRiv from '../../example/assets/rive/GradientBorder.riv';
+import outOfBandRiv from '../../example/assets/rive/out_of_band.riv';
import blinkoRiv from '../../example/assets/rive/blinko.riv';
import rewardsRiv from '../../example/assets/rive/rewards.riv';
import fallbackFontsRiv from '../../example/assets/rive/fallback_fonts.riv';
@@ -451,3 +454,68 @@ expectAssignable(
rewardsVM.numberProperty('Item_Value_Icon/Item_Value')
);
expectError(rewardsVM.numberProperty('Item_Value_Icon/DoesNotExist'));
+
+
+// ============================================================
+// Typed referencedAssets (out-of-band assets from the schema)
+// ============================================================
+
+declare const riveImage: RiveImage;
+
+// Valid keys with correct types compile; RiveImage allowed for image assets
+useRiveFile(outOfBandRiv, {
+ referencedAssets: {
+ 'Inter-594377': { source: 1, type: 'font' },
+ 'referenced-image-2929282': { source: { uri: 'https://x/i.png' } },
+ 'hosted_audio-2989208': { source: 2, type: 'audio' },
+ },
+});
+useRiveFile(outOfBandRiv, {
+ referencedAssets: { 'cdn-image-2989123': riveImage },
+});
+
+// Unknown asset key is a hard error (must not fall through to the untyped
+// number-input signature)
+expectError(
+ useRiveFile(outOfBandRiv, {
+ referencedAssets: { 'Inter-59437': { source: 1, type: 'font' } },
+ })
+);
+
+// Declared type must match the asset's actual kind
+expectError(
+ useRiveFile(outOfBandRiv, {
+ referencedAssets: { 'Inter-594377': { source: 1, type: 'image' } },
+ })
+);
+
+// RiveImage objects are only accepted for image assets
+expectError(
+ useRiveFile(outOfBandRiv, {
+ referencedAssets: { 'Inter-594377': riveImage },
+ })
+);
+
+// A schema with no referenced assets accepts no keys at all
+expectError(
+ useRiveFile(rewardsRiv, {
+ referencedAssets: { anything: { source: 1, type: 'font' } },
+ })
+);
+
+// Untyped inputs keep accepting arbitrary keys (backward compat) — every
+// legacy input form: url string, uri object, require() (any), plain number.
+useRiveFile('https://example.com/a.riv', {
+ referencedAssets: { anything: { source: 1, type: 'font' } },
+});
+useRiveFile({ uri: 'file:///a.riv' }, {
+ referencedAssets: { whatever: { source: 1 } },
+});
+declare const requireResult: any;
+useRiveFile(requireResult, {
+ referencedAssets: { anything: { source: 1, type: 'font' } },
+});
+declare const plainAssetId: number;
+useRiveFile(plainAssetId, {
+ referencedAssets: { anything: { source: 1, type: 'audio' } },
+});
diff --git a/src/core/ReferencedAssets.ts b/src/core/ReferencedAssets.ts
index 48d11ea5..b9bb763b 100644
--- a/src/core/ReferencedAssets.ts
+++ b/src/core/ReferencedAssets.ts
@@ -3,6 +3,7 @@ import type {
RiveAssetType,
} from '../specs/RiveFile.nitro';
import type { RiveImage } from '../specs/RiveImage.nitro';
+import type { RiveFileSchema } from './TypedRiveFile';
export type ReferencedAssetSource = {
source: number | { uri: string };
@@ -24,3 +25,44 @@ export interface ReferencedAssets {
export type ResolvedReferencedAssets = {
[assetName: string]: ResolvedReferencedAsset;
};
+
+/**
+ * `referencedAssets` constrained to a generated schema: keys must be asset
+ * unique identifiers from the .riv file, and each entry's declared `type`
+ * must match the asset's actual kind. `RiveImage` objects are only accepted
+ * for image assets. Degrades to the untyped {@link ReferencedAssets} when the
+ * schema is not statically known.
+ */
+export type TypedReferencedAssets =
+ string extends Extract
+ ? ReferencedAssets
+ : [Extract] extends [never]
+ ? // No referenced assets in the file. An empty mapped type would be
+ // `{}`, which accepts any object — and a `never` value would reject
+ // entries with an unexplained "not assignable to never". The literal
+ // string type below rejects real entries too, but surfaces the reason
+ // in the compiler error itself.
+ Record
+ : {
+ [K in Extract<
+ keyof S['assets'],
+ string
+ >]?: S['assets'][K] extends 'image'
+ ? (ReferencedAssetSource & { type?: 'image' }) | RiveImage
+ : ReferencedAssetSource & {
+ type?: S['assets'][K] & RiveAssetType;
+ };
+ };
+
+/**
+ * Resolved (post-`Image.resolveAssetSource`) form of
+ * {@link TypedReferencedAssets}, used by the RiveFileFactory methods.
+ */
+export type TypedResolvedReferencedAssets =
+ string extends Extract
+ ? ResolvedReferencedAssets
+ : [Extract] extends [never]
+ ? Record
+ : {
+ [K in Extract]?: ResolvedReferencedAsset;
+ };
diff --git a/src/core/RiveFile.ts b/src/core/RiveFile.ts
index 75e114ae..c4596747 100644
--- a/src/core/RiveFile.ts
+++ b/src/core/RiveFile.ts
@@ -3,7 +3,10 @@ import type { RiveFileFactory as RiveFileFactoryInternal } from '../specs/RiveFi
import type { RiveAsset, RiveFileSchema, TypedRiveFile } from './TypedRiveFile';
import { Image } from 'react-native';
-import type { ResolvedReferencedAssets } from './ReferencedAssets';
+import type {
+ ResolvedReferencedAssets,
+ TypedResolvedReferencedAssets,
+} from './ReferencedAssets';
const RiveFileInternal =
NitroModules.createHybridObject('RiveFileFactory');
@@ -156,7 +159,7 @@ export namespace RiveFileFactory {
*/
export async function fromSource(
source: RiveAsset,
- referencedAssets: ResolvedReferencedAssets | undefined,
+ referencedAssets: TypedResolvedReferencedAssets | undefined,
loadCdn?: boolean
): Promise>;
export async function fromSource(
diff --git a/src/core/TypedRiveFile.ts b/src/core/TypedRiveFile.ts
index 82ae93f1..0be35f0f 100644
--- a/src/core/TypedRiveFile.ts
+++ b/src/core/TypedRiveFile.ts
@@ -16,6 +16,12 @@ export interface RiveFileSchema {
defaultArtboard: string;
stateMachines: Record;
viewModels: Record>;
+ /**
+ * Non-embedded (referenced or CDN) assets that the app must supply via
+ * `referencedAssets`: unique asset identifier (e.g. 'Inter-594377') →
+ * 'image' | 'font' | 'audio'.
+ */
+ assets: Record;
}
/**
diff --git a/src/hooks/useRiveFile.ts b/src/hooks/useRiveFile.ts
index 77449754..78b2690e 100644
--- a/src/hooks/useRiveFile.ts
+++ b/src/hooks/useRiveFile.ts
@@ -11,6 +11,7 @@ import type {
ReferencedAsset,
ReferencedAssets,
ResolvedReferencedAssets,
+ TypedReferencedAssets,
} from '../core/ReferencedAssets';
import type {
RiveAsset,
@@ -97,17 +98,26 @@ export type UseRiveFileResult =
| { riveFile: null; isLoading: false; error: Error }
| { riveFile: undefined; isLoading: true; error: null };
-export function useRiveFile(
- input: RiveAsset,
- options?: UseRiveFileOptions
-):
- | { riveFile: TypedRiveFile; isLoading: false; error: null }
- | { riveFile: null; isLoading: false; error: Error }
- | { riveFile: undefined; isLoading: true; error: null };
-export function useRiveFile(
- input: RiveFileInput | undefined,
- options?: UseRiveFileOptions
-): UseRiveFileResult;
+/**
+ * Result of {@link useRiveFile}: typed when the input is a generated
+ * RiveAsset, the plain untyped result otherwise.
+ */
+type UseRiveFileResultFor =
+ string extends T['artboards']
+ ? UseRiveFileResult
+ :
+ | { riveFile: TypedRiveFile; isLoading: false; error: null }
+ | { riveFile: null; isLoading: false; error: Error }
+ | { riveFile: undefined; isLoading: true; error: null };
+
+// Single generic signature (not a typed + untyped overload pair): an invalid
+// referencedAssets key or type on a schema-typed asset must be a hard error
+// here — with separate overloads it would silently fall through to the
+// untyped one, because a RiveAsset is assignable to the plain number input.
+export function useRiveFile(
+ input: RiveAsset | Exclude | undefined,
+ options?: { referencedAssets?: TypedReferencedAssets }
+): UseRiveFileResultFor;
export function useRiveFile(
input: RiveFileInput | undefined,
options: UseRiveFileOptions = {}
diff --git a/src/index.tsx b/src/index.tsx
index 6dc2675c..403a4c59 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -88,6 +88,10 @@ export {
type UseViewModelInstanceRequiredResult,
} from './hooks/useViewModelInstance';
export { useRiveFile, type UseRiveFileResult } from './hooks/useRiveFile';
+export type {
+ TypedReferencedAssets,
+ TypedResolvedReferencedAssets,
+} from './core/ReferencedAssets';
export { type RiveFileInput } from './hooks/useRiveFile';
export { type SetValueAction } from './types';
export { RiveRuntime } from './core/RiveRuntime';