diff --git a/example/assets/rive/GradientBorder.riv.d.ts b/example/assets/rive/GradientBorder.riv.d.ts index 8e9d41ec..02de2ce8 100644 --- a/example/assets/rive/GradientBorder.riv.d.ts +++ b/example/assets/rive/GradientBorder.riv.d.ts @@ -3,6 +3,7 @@ // Source: GradientBorder.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Layout'; defaultArtboard: 'Layout'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ isFocused: 'boolean'; }; }; + referencedAssets: {}; }>; 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 225a4f90..f266b571 100644 --- a/example/assets/rive/arbtboards-models-instances.riv.d.ts +++ b/example/assets/rive/arbtboards-models-instances.riv.d.ts @@ -3,6 +3,7 @@ // Source: arbtboards-models-instances.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'artboard1' | 'artboard3' | 'artboard2' | 'artboardc1'; defaultArtboard: 'artboard1'; stateMachines: { @@ -23,5 +24,6 @@ declare const asset: RiveAsset<{ _id: 'string'; }; }; + referencedAssets: {}; }>; 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 4c3eb1e0..066cb76c 100644 --- a/example/assets/rive/artboard_db_test.riv.d.ts +++ b/example/assets/rive/artboard_db_test.riv.d.ts @@ -3,6 +3,7 @@ // Source: artboard_db_test.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Main' | 'ArtboardGreen' | 'ArtboardBlue' | 'ArtboardRed'; defaultArtboard: 'Main'; stateMachines: { @@ -18,5 +19,6 @@ declare const asset: RiveAsset<{ artboard_2: 'artboard'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/blinko.riv.d.ts b/example/assets/rive/blinko.riv.d.ts index bc077aca..c25767ca 100644 --- a/example/assets/rive/blinko.riv.d.ts +++ b/example/assets/rive/blinko.riv.d.ts @@ -3,6 +3,7 @@ // Source: blinko.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Main' | 'StoreItem' | 'Token' | 'Indicator' | 'Puck' | 'StoreIcon' | 'Peg' | 'scoreBox'; defaultArtboard: 'Main'; stateMachines: { @@ -97,5 +98,6 @@ declare const asset: RiveAsset<{ 'property of gameLogicVM': 'viewModel:gameLogicVM'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/bouncing_ball.riv.d.ts b/example/assets/rive/bouncing_ball.riv.d.ts index 7e193b35..0cf65904 100644 --- a/example/assets/rive/bouncing_ball.riv.d.ts +++ b/example/assets/rive/bouncing_ball.riv.d.ts @@ -3,6 +3,7 @@ // Source: bouncing_ball.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ ypos: 'number'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/click-count.riv.d.ts b/example/assets/rive/click-count.riv.d.ts index b7f43555..92a34c83 100644 --- a/example/assets/rive/click-count.riv.d.ts +++ b/example/assets/rive/click-count.riv.d.ts @@ -3,6 +3,7 @@ // Source: click-count.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ clickCount: 'number'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/counter.riv.d.ts b/example/assets/rive/counter.riv.d.ts index 82927ffe..5a0771ed 100644 --- a/example/assets/rive/counter.riv.d.ts +++ b/example/assets/rive/counter.riv.d.ts @@ -3,6 +3,7 @@ // Source: counter.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ cnt: 'number'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/databinding.riv.d.ts b/example/assets/rive/databinding.riv.d.ts index f3aae26d..943470e6 100644 --- a/example/assets/rive/databinding.riv.d.ts +++ b/example/assets/rive/databinding.riv.d.ts @@ -3,6 +3,7 @@ // Source: databinding.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -27,5 +28,6 @@ declare const asset: RiveAsset<{ name: 'string'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/databinding_images.riv.d.ts b/example/assets/rive/databinding_images.riv.d.ts index 1ecb298a..9d8e690f 100644 --- a/example/assets/rive/databinding_images.riv.d.ts +++ b/example/assets/rive/databinding_images.riv.d.ts @@ -3,6 +3,7 @@ // Source: databinding_images.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -11,8 +12,9 @@ declare const asset: RiveAsset<{ enums: {}; viewModels: { MyViewModel: { - bound_image: 'image'; + bound_image: 'assetImage'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/databinding_lists.riv.d.ts b/example/assets/rive/databinding_lists.riv.d.ts index b11c0ec1..786e52b1 100644 --- a/example/assets/rive/databinding_lists.riv.d.ts +++ b/example/assets/rive/databinding_lists.riv.d.ts @@ -3,6 +3,7 @@ // Source: databinding_lists.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -17,5 +18,6 @@ declare const asset: RiveAsset<{ team: 'list'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/fallback_fonts.riv.d.ts b/example/assets/rive/fallback_fonts.riv.d.ts index dfbf4cb5..62daec5d 100644 --- a/example/assets/rive/fallback_fonts.riv.d.ts +++ b/example/assets/rive/fallback_fonts.riv.d.ts @@ -3,6 +3,7 @@ // Source: fallback_fonts.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/finished_trigger.riv.d.ts b/example/assets/rive/finished_trigger.riv.d.ts index b1b61298..76fc5161 100644 --- a/example/assets/rive/finished_trigger.riv.d.ts +++ b/example/assets/rive/finished_trigger.riv.d.ts @@ -3,6 +3,7 @@ // Source: finished_trigger.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ finished: 'trigger'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/font_fallback.riv.d.ts b/example/assets/rive/font_fallback.riv.d.ts index c7df2170..26892c14 100644 --- a/example/assets/rive/font_fallback.riv.d.ts +++ b/example/assets/rive/font_fallback.riv.d.ts @@ -3,6 +3,7 @@ // Source: font_fallback.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ text: 'string'; }; }; + referencedAssets: {}; }>; 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 2d6a1e5a..3bf20587 100644 --- a/example/assets/rive/hello_world_text.riv.d.ts +++ b/example/assets/rive/hello_world_text.riv.d.ts @@ -3,6 +3,7 @@ // Source: hello_world_text.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'New Artboard'; defaultArtboard: 'New Artboard'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/inputglow.riv.d.ts b/example/assets/rive/inputglow.riv.d.ts index 12ce5ed9..6b718751 100644 --- a/example/assets/rive/inputglow.riv.d.ts +++ b/example/assets/rive/inputglow.riv.d.ts @@ -3,6 +3,7 @@ // Source: inputglow.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Layout'; defaultArtboard: 'Layout'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ isFocused: 'boolean'; }; }; + referencedAssets: {}; }>; 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 3e3bc792..0fd8c4eb 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 @@ -3,6 +3,7 @@ // Source: ios_android_layouts_demo_v01.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'iOS_Android_Layouts_demo_v01'; defaultArtboard: 'iOS_Android_Layouts_demo_v01'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/layout_test.riv.d.ts b/example/assets/rive/layout_test.riv.d.ts index dc5b67de..7981af24 100644 --- a/example/assets/rive/layout_test.riv.d.ts +++ b/example/assets/rive/layout_test.riv.d.ts @@ -3,6 +3,7 @@ // Source: layout_test.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/layouts_demo.riv.d.ts b/example/assets/rive/layouts_demo.riv.d.ts index 9443f4e1..6cbcbe4f 100644 --- a/example/assets/rive/layouts_demo.riv.d.ts +++ b/example/assets/rive/layouts_demo.riv.d.ts @@ -3,6 +3,7 @@ // Source: layouts_demo.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'iOS_Android_Layouts_demo_v01'; defaultArtboard: 'iOS_Android_Layouts_demo_v01'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/many_viewmodels.riv.d.ts b/example/assets/rive/many_viewmodels.riv.d.ts index bf0191f9..4569d6a9 100644 --- a/example/assets/rive/many_viewmodels.riv.d.ts +++ b/example/assets/rive/many_viewmodels.riv.d.ts @@ -3,6 +3,7 @@ // Source: many_viewmodels.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -11,10 +12,11 @@ declare const asset: RiveAsset<{ enums: {}; viewModels: { First: { - imageValue: 'image'; + imageValue: 'assetImage'; colorValue: 'color'; textValue: 'string'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/movecircle.riv.d.ts b/example/assets/rive/movecircle.riv.d.ts index b156f66e..075c1cc7 100644 --- a/example/assets/rive/movecircle.riv.d.ts +++ b/example/assets/rive/movecircle.riv.d.ts @@ -3,6 +3,7 @@ // Source: movecircle.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ posY: 'number'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/nodefaultbouncing.riv.d.ts b/example/assets/rive/nodefaultbouncing.riv.d.ts index 63856505..de3df2ba 100644 --- a/example/assets/rive/nodefaultbouncing.riv.d.ts +++ b/example/assets/rive/nodefaultbouncing.riv.d.ts @@ -3,6 +3,7 @@ // Source: nodefaultbouncing.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -14,5 +15,6 @@ declare const asset: RiveAsset<{ }; }; + referencedAssets: {}; }>; 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 93f933ea..8d386cee 100644 --- a/example/assets/rive/on_entry_test.riv.d.ts +++ b/example/assets/rive/on_entry_test.riv.d.ts @@ -3,6 +3,7 @@ // Source: on_entry_test.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -15,5 +16,8 @@ declare const asset: RiveAsset<{ inputOnEntry: 'number'; }; }; + referencedAssets: { + '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 766bc851..8b49379c 100644 --- a/example/assets/rive/out_of_band.riv.d.ts +++ b/example/assets/rive/out_of_band.riv.d.ts @@ -3,6 +3,7 @@ // Source: out_of_band.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -10,5 +11,13 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: { + '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 ac069c38..3402de57 100644 --- a/example/assets/rive/quick_start.riv.d.ts +++ b/example/assets/rive/quick_start.riv.d.ts @@ -3,6 +3,7 @@ // Source: quick_start.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'health_bar_v01'; defaultArtboard: 'health_bar_v01'; stateMachines: { @@ -18,5 +19,6 @@ declare const asset: RiveAsset<{ health: 'number'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/rating.riv.d.ts b/example/assets/rive/rating.riv.d.ts index 7b7c9c4a..86173763 100644 --- a/example/assets/rive/rating.riv.d.ts +++ b/example/assets/rive/rating.riv.d.ts @@ -3,6 +3,7 @@ // Source: rating.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'New Artboard'; defaultArtboard: 'New Artboard'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/rewards.riv.d.ts b/example/assets/rive/rewards.riv.d.ts index c90a9cfa..ad339e67 100644 --- a/example/assets/rive/rewards.riv.d.ts +++ b/example/assets/rive/rewards.riv.d.ts @@ -3,6 +3,7 @@ // Source: rewards.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Main' | 'Lives 2' | 'Item Value' | 'Chest' | 'Button' | 'Heart' | 'Item'; defaultArtboard: 'Main'; stateMachines: { @@ -50,5 +51,6 @@ declare const asset: RiveAsset<{ Item_Selection: 'enum:Item_Selection'; }; }; + referencedAssets: {}; }>; 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 fbffcf2e..b4a55589 100644 --- a/example/assets/rive/style_fallback_fonts.riv.d.ts +++ b/example/assets/rive/style_fallback_fonts.riv.d.ts @@ -3,6 +3,7 @@ // Source: style_fallback_fonts.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/tabtest.riv.d.ts b/example/assets/rive/tabtest.riv.d.ts index e2a706d9..13ad33dc 100644 --- a/example/assets/rive/tabtest.riv.d.ts +++ b/example/assets/rive/tabtest.riv.d.ts @@ -3,6 +3,7 @@ // Source: tabtest.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -16,5 +17,6 @@ declare const asset: RiveAsset<{ enumProperty: 'enum:Selected'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/viewmodelproperty.riv.d.ts b/example/assets/rive/viewmodelproperty.riv.d.ts index e7398f8d..05def55f 100644 --- a/example/assets/rive/viewmodelproperty.riv.d.ts +++ b/example/assets/rive/viewmodelproperty.riv.d.ts @@ -3,6 +3,7 @@ // Source: viewmodelproperty.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -18,5 +19,6 @@ declare const asset: RiveAsset<{ name: 'string'; }; }; + referencedAssets: {}; }>; 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 62ee2ff1..0bdc7283 100644 --- a/example/assets/rive/vm_value_change_test.riv.d.ts +++ b/example/assets/rive/vm_value_change_test.riv.d.ts @@ -3,6 +3,7 @@ // Source: vm_value_change_test.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Artboard'; defaultArtboard: 'Artboard'; stateMachines: { @@ -15,5 +16,6 @@ declare const asset: RiveAsset<{ textValue: 'string'; }; }; + referencedAssets: {}; }>; 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 7b7c9c4a..86173763 100644 --- a/expo-example/assets/rive/rating.riv.d.ts +++ b/expo-example/assets/rive/rating.riv.d.ts @@ -3,6 +3,7 @@ // Source: rating.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'New Artboard'; defaultArtboard: 'New Artboard'; stateMachines: { @@ -10,5 +11,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + referencedAssets: {}; }>; 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 ae121d9c..44645991 100644 --- a/expo-example/assets/rive/rewards_source.riv.d.ts +++ b/expo-example/assets/rive/rewards_source.riv.d.ts @@ -3,6 +3,7 @@ // Source: rewards_source.riv import type { RiveAsset } from '@rive-app/react-native'; declare const asset: RiveAsset<{ + schemaVersion: 1; artboards: 'Main' | 'Lives 2' | 'Item Value' | 'Chest' | 'Button' | 'Heart' | 'Item'; defaultArtboard: 'Main'; stateMachines: { @@ -50,5 +51,6 @@ declare const asset: RiveAsset<{ Item_Selection: 'enum:Item_Selection'; }; }; + referencedAssets: {}; }>; export default asset; diff --git a/scripts/__tests__/rive-extract-schema.test.ts b/scripts/__tests__/rive-extract-schema.test.ts index d9aec246..2c20659b 100644 --- a/scripts/__tests__/rive-extract-schema.test.ts +++ b/scripts/__tests__/rive-extract-schema.test.ts @@ -54,6 +54,24 @@ describe('rive-extract-schema', () => { expect(vms.Energy_Bar!.Bar_Color).toBe('color'); }); + test('extracts non-embedded assets by uniqueName and kind', () => { + const oob = extract( + resolve(import.meta.dirname, '../../example/assets/rive/out_of_band.riv') + ); + expect(oob.referencedAssets).toEqual({ + 'Inter-594377': 'font', + 'referenced-image-2929282': 'image', + 'referenced_audio-2929340': 'audio', + 'hosted_audio-2989208': 'audio', + 'Damion-593587': 'font', + 'cdn-image-2989123': 'image', + }); + }); + + test('embedded-only files report no referenced assets', () => { + expect(schema.referencedAssets).toEqual({}); + }); + test('enum properties reference a file-level enum definition', () => { const databinding = extract( resolve(import.meta.dirname, '../../example/assets/rive/databinding.riv') diff --git a/scripts/__tests__/rive-gen-emit.test.ts b/scripts/__tests__/rive-gen-emit.test.ts index 6920218d..7c8fddb3 100644 --- a/scripts/__tests__/rive-gen-emit.test.ts +++ b/scripts/__tests__/rive-gen-emit.test.ts @@ -7,7 +7,10 @@ import { unionRecord, vmRecord, schemaBody, + assetsRecord, + classifyAsset, enumPropTypeString, + propertyTypeString, collectEnums, viewModelRefTypeString, type Schema, @@ -50,6 +53,7 @@ describe('emit escaping', () => { viewModels: { "It's VM": { "quote'": "enum:Pet's" }, }, + referencedAssets: { "Font's-123": 'font' }, }; const body = schemaBody(schema); expect(parseErrors(`declare const asset: {\n${body}\n};`)).toEqual([]); @@ -84,10 +88,21 @@ describe('schemaBody', () => { stateMachines: { Main: ['SM'] }, enums: {}, viewModels: {}, + referencedAssets: {}, }; - test('always emits viewModels, empty object when none', () => { + test('always emits viewModels and referencedAssets, empty objects when none', () => { expect(schemaBody(base)).toContain('viewModels: {};'); + expect(schemaBody(base)).toContain('referencedAssets: {};'); + }); + + test('emits referencedAssets record when present', () => { + const body = schemaBody({ + ...base, + referencedAssets: { 'Inter-594377': 'font', 'img-1': 'image' }, + }); + expect(body).toContain("'Inter-594377': 'font';"); + expect(body).toContain("'img-1': 'image';"); }); test('always emits enums, empty object when none', () => { @@ -193,3 +208,60 @@ 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';"); + }); +}); + +describe('propertyTypeString', () => { + test("names image properties 'assetImage', passes the rest through", () => { + expect(propertyTypeString('image')).toBe('assetImage'); + expect(propertyTypeString('number')).toBe('number'); + expect(propertyTypeString('artboard')).toBe('artboard'); + }); +}); diff --git a/scripts/__tests__/rive-gen-fuzz.test.ts b/scripts/__tests__/rive-gen-fuzz.test.ts index fba72d08..caee15ed 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 ed183709..10214557 100644 --- a/scripts/rive-extract-schema.ts +++ b/scripts/rive-extract-schema.ts @@ -1,143 +1,31 @@ /** - * Extracts artboard names and state machines from a .riv file and prints JSON to stdout. + * Prints the schema of a .riv file as JSON: artboards, state machines, enums, + * view models and referenced assets — the same data rive-gen-types emits as + * a .riv.d.ts. * * Usage: * node scripts/rive-extract-schema.ts path/to/file.riv * node scripts/rive-extract-schema.ts https://example.com/file.riv - * - * Output: { "artboards": ["Main", "Intro"], "stateMachines": { "Main": ["State Machine 1"], "Intro": ["Idle"] } } */ -import { readFileSync } from 'fs'; -// Default-import + destructure: @rive-app/canvas is CJS, and Node's ESM -// loader cannot statically see its named exports (bun's interop can). -import riveCanvas from '@rive-app/canvas'; -const { RuntimeLoader } = riveCanvas; -import { - collectEnums, - enumPropTypeString, - nameMap, - viewModelRefTypeString, - type RuntimeProperty, -} from './rive-gen-types.ts'; +import { extractSchema, setupWasmShims } from './rive-gen-types.ts'; -// noUncheckedIndexedAccess: process.argv destructuring yields string | undefined const input: string | undefined = process.argv[2]; -// @rive-app/canvas checks for browser globals during WASM initialisation. -// We only call file-inspection APIs (no rendering), so minimal shims are enough. -(globalThis as any).document = { - createElement: () => ({ getContext: () => null }), -}; -(globalThis as any).Image = class {}; - -// The WASM runtime prints warnings (e.g. "No WebGL support") via console. -// Redirect them to stderr so our JSON output stays clean. -console.log = (...args: unknown[]) => - process.stderr.write(args.join(' ') + '\n'); -console.warn = (...args: unknown[]) => - process.stderr.write(args.join(' ') + '\n'); - -// Catch errors from microtasks/unhandled rejections that would otherwise cause -// a silent exit with code 0 on some Bun versions (e.g. when img.la() throws in -// a queueMicrotask callback because WebGL is unavailable on Linux CI). -process.on('uncaughtException', (err) => { - process.stderr.write(`uncaughtException: ${err.stack ?? err.message}\n`); - process.exit(1); -}); -process.on('unhandledRejection', (reason) => { - process.stderr.write(`unhandledRejection: ${reason}\n`); - process.exit(1); -}); - if (!input) { - process.stderr.write('Usage: bun rive-extract-schema.ts \n'); + process.stderr.write('Usage: node rive-extract-schema.ts \n'); process.exit(1); } -const source: string = input; - -async function loadBytes(source: string): Promise { - if (source.startsWith('http://') || source.startsWith('https://')) { - const res = await fetch(source); - if (!res.ok) throw new Error(`HTTP ${res.status}: ${source}`); - return new Uint8Array(await res.arrayBuffer()); - } - return new Uint8Array(readFileSync(source)); -} - -async function main() { - const bytes = await loadBytes(source); - const runtime = await RuntimeLoader.awaitInstance(); - // Claim every referenced asset without decoding it. We only introspect - // 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 assetLoader = new (runtime as any).CustomFileAssetLoader({ - loadContents: () => true, +// Also routes the WASM runtime's console output to stderr, keeping stdout +// pure JSON. +setupWasmShims(); + +extractSchema(input) + .then((schema) => { + process.stdout.write(JSON.stringify(schema, null, 2) + '\n'); + }) + .catch((err: Error) => { + process.stderr.write(err.message + '\n'); + process.exit(1); }); - - let timer: ReturnType | undefined; - const riveFile = await Promise.race([ - runtime.load(bytes, assetLoader, false), - new Promise((_, reject) => { - timer = setTimeout( - () => reject(new Error('load() timed out after 30000ms')), - 30_000 - ); - }), - ]).finally(() => clearTimeout(timer)); - - // load() resolves null (rather than rejecting) for unparseable bytes. - if (!riveFile) { - throw new Error('not a valid .riv file (load() returned null)'); - } - - const artboards: string[] = []; - const stateMachines = nameMap(); - for (let i = 0; i < riveFile.artboardCount(); i++) { - const artboard = riveFile.artboardByIndex(i); - artboards.push(artboard.name); - const sms: string[] = []; - for (let j = 0; j < artboard.stateMachineCount(); j++) { - sms.push(artboard.stateMachineByIndex(j).name); - } - stateMachines[artboard.name] = sms; - } - - const enums = collectEnums(riveFile); - - const viewModels = nameMap>(); - const vmCount = (riveFile as any).viewModelCount() as number; - for (let i = 0; i < vmCount; i++) { - const vm = (riveFile as any).viewModelByIndex(i); - const properties = vm.getProperties() as RuntimeProperty[]; - // Create a blank instance to resolve viewModel property references - const inst = vm.instance?.() as any; - const props = nameMap(); - for (const p of properties) { - if (p.type === 'viewModel') { - props[p.name] = viewModelRefTypeString(inst, p.name); - } else if (p.type === 'enumType') { - props[p.name] = enumPropTypeString(p, enums); - } else { - props[p.name] = p.type; - } - } - viewModels[vm.name] = props; - } - - const defaultArtboard = artboards[0] ?? ''; - process.stdout.write( - JSON.stringify( - { artboards, defaultArtboard, stateMachines, enums, viewModels }, - null, - 2 - ) + '\n' - ); -} - -main().catch((err: Error) => { - process.stderr.write(err.message + '\n'); - process.exit(1); -}); diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index c9707978..78b25593 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -28,7 +28,7 @@ import { pathToFileURL } from 'url'; // Called from main() so that importing this module (for unit-testing the // exported emit helpers) has no global side effects. -function setupWasmShims(): void { +export function setupWasmShims(): void { // Browser shims required by the @rive-app/canvas WASM runtime. (globalThis as any).document = { createElement: () => ({ getContext: () => null }), @@ -49,6 +49,45 @@ export interface Schema { /** File-level enum definitions, name → values. */ enums: Record; viewModels: Record>; + /** Non-embedded (referenced or hosted) assets: runtime `uniqueName` → kind. */ + referencedAssets: Record; +} + +/** + * Classify a file asset reported by the WASM asset loader. Only non-embedded + * assets (referenced or hosted) are schema-relevant: they are the ones an + * app must supply via `referencedAssets` — the new runtime does not fetch + * hosted assets itself. The key is the asset's `uniqueName` (`uniqueFilename` + * minus extension, e.g. 'Inter-594377'), 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; } export interface RuntimeProperty { @@ -87,7 +126,7 @@ async function getRuntime(): Promise { /** Per-file guard: a stalled WASM load() must fail loudly, never hang the batch. */ const LOAD_TIMEOUT_MS = 30_000; -async function extractSchema(input: string): Promise { +export async function extractSchema(input: string): Promise { let bytes: Uint8Array; if (input.startsWith('http://') || input.startsWith('https://')) { const res = await fetch(input); @@ -103,9 +142,7 @@ 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. - const assetLoader = new (runtime as any).CustomFileAssetLoader({ - loadContents: () => true, - }); + const { assetLoader, assets } = createAssetCollector(runtime); let timer: ReturnType | undefined; const riveFile = await Promise.race([ @@ -150,7 +187,7 @@ async function extractSchema(input: string): Promise { } else if (p.type === 'enumType') { props[p.name] = enumPropTypeString(p, enums); } else { - props[p.name] = p.type; + props[p.name] = propertyTypeString(p.type); } } viewModels[vm.name] = props; @@ -162,6 +199,7 @@ async function extractSchema(input: string): Promise { stateMachines, enums, viewModels, + referencedAssets: assets, }; } @@ -209,23 +247,59 @@ export function vmRecord( .join('\n'); } +/** + * Asset loader that claims every asset without decoding it and records the + * non-embedded ones — load() callbacks are the only place asset metadata is + * visible. + */ +export function createAssetCollector(runtime: any): { + assetLoader: any; + assets: Record; +} { + const assets = nameMap(); + const assetLoader = new runtime.CustomFileAssetLoader({ + loadContents: (asset: any, embeddedBytes: Uint8Array | undefined) => { + const classified = classifyAsset(asset ?? {}, embeddedBytes?.length ?? 0); + if (classified) assets[classified.id] = classified.kind; + return true; + }, + }); + return { assetLoader, assets }; +} + +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'); +} + +/** Must match `RiveFileSchema['schemaVersion']` in src/core/TypedRiveFile.ts. */ +export const SCHEMA_VERSION = 1; + export function schemaBody(schema: Schema): string { const enumSection = Object.keys(schema.enums).length > 0 ? `\n enums: {\n${unionRecord(schema.enums)}\n };` : '\n enums: {};'; - // Always emit viewModels — omitting it would fail the RiveFileSchema - // constraint and silently degrade the whole asset to untyped. + // Always emit viewModels/referencedAssets — omitting either fails the + // RiveFileSchema check, and the import turns into an "out of date" error. const vmSection = Object.keys(schema.viewModels).length > 0 ? `\n viewModels: {\n${vmRecord(schema.viewModels)}\n };` : '\n viewModels: {};'; + const assetSection = + Object.keys(schema.referencedAssets).length > 0 + ? `\n referencedAssets: {\n${assetsRecord(schema.referencedAssets)}\n };` + : '\n referencedAssets: {};'; return `\ + schemaVersion: ${SCHEMA_VERSION}; artboards: ${schema.artboards.map(strLit).join(' | ')}; defaultArtboard: ${strLit(schema.defaultArtboard)}; stateMachines: { ${unionRecord(schema.stateMachines)} - };${enumSection}${vmSection}`; + };${enumSection}${vmSection}${assetSection}`; } function dtsContent(input: string, schema: Schema): string { @@ -344,6 +418,16 @@ export function collectEnums(riveFile: any): Record { return enums; } +/** + * Schema type string for a primitive property. The web runtime reports image + * properties as `'image'`; the schema uses `'assetImage'`, matching RML's + * `ViewModelPropertyAssetImage` and the native `ViewModelPropertyType`, and + * keeping it distinct from the `'image'` referenced-asset kind. + */ +export function propertyTypeString(runtimeType: string): string { + return runtimeType === 'image' ? 'assetImage' : runtimeType; +} + /** * Schema type string for an enum property: a reference to the file-level * enum it uses (`'enum:Pets'`), or untyped `'enum'` when the runtime reports diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts index 807d251a..51e7c3a5 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -26,10 +26,15 @@ 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 { RiveFileFactory } from '../../src/core/RiveFile'; +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'; +import databindingImagesRiv from '../../example/assets/rive/databinding_images.riv'; // Infer schemas from the generated .riv.d.ts assets type GradientBorderSchema = typeof gradientBorderRiv extends RiveAsset @@ -262,20 +267,24 @@ expectError(null as unknown as EnumValues); // An untyped 'enum' property (a built-in enum) accepts any string. type BuiltInEnumSchema = { + schemaVersion: 1; artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; enums: {}; viewModels: { VM: { blend: 'enum' } }; + referencedAssets: {}; }; declare const builtInVM: TypedViewModelInstance; expectType>(useRiveEnum('blend', builtInVM)); // A named reference missing from `enums` is never — not the enum's name. type DanglingSchema = { + schemaVersion: 1; artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; + referencedAssets: {}; enums: {}; viewModels: { VM: { pet: 'enum:Pets' } }; }; @@ -283,9 +292,11 @@ expectType(null as unknown as EnumValuesOf); // An enum with no values degrades to an untyped (string) property. type EmptyEnumSchema = { + schemaVersion: 1; artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; + referencedAssets: {}; enums: { E: never }; viewModels: { VM: { pet: 'enum:E' } }; }; @@ -495,3 +506,139 @@ 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' } }, +}); + +// RiveFileFactory.fromSource applies the same checks (resolved asset form) +// instead of falling through to an untyped overload. +expectError( + RiveFileFactory.fromSource(outOfBandRiv, { + 'Inter-59437': { sourceUrl: 'https://x/f.ttf' }, + }) +); +expectError( + RiveFileFactory.fromSource(outOfBandRiv, { + 'Inter-594377': { sourceUrl: 'https://x/f.ttf', type: 'image' }, + }) +); +expectError( + RiveFileFactory.fromSource(outOfBandRiv, { + 'Inter-594377': { image: riveImage }, + }) +); +expectError( + RiveFileFactory.fromSource(rewardsRiv, { + anything: { sourceUrl: 'https://x/f.ttf' }, + }) +); +expectType>>( + RiveFileFactory.fromSource(outOfBandRiv, { + 'Inter-594377': { sourceUrl: 'https://x/f.ttf', type: 'font' }, + 'cdn-image-2989123': { image: riveImage }, + }) +); +RiveFileFactory.fromSource(plainAssetId, { anything: { sourceUrl: 'x' } }); +RiveFileFactory.fromSource({ uri: 'https://x/a.riv' }, { anything: {} }); + +// A standalone hand-written schema (literal ViewModels, string artboards) +// keeps its ViewModel typing through useRiveFile. +type HandWrittenSchema = { + schemaVersion: 1; + artboards: string; + defaultArtboard: string; + stateMachines: Record; + enums: {}; + referencedAssets: {}; + viewModels: { VM: { n: 'number' } }; +}; +declare const handWrittenAsset: RiveAsset; +{ + const { riveFile } = useRiveFile(handWrittenAsset); + expectError( + useViewModelInstance(riveFile, { viewModelName: 'NotAVM', async: true }) + ); +} + +// Image properties are 'assetImage' (RML ViewModelPropertyAssetImage), distinct +// from the 'image' referenced-asset kind. +type ImagesSchema = + typeof databindingImagesRiv extends RiveAsset ? T : never; +declare const imagesVM: TypedViewModelInstance; +imagesVM.imageProperty('bound_image'); +expectError(imagesVM.numberProperty('bound_image')); + +// A .riv.d.ts from an older generator (no or a different schemaVersion) +// fails at the use site instead of silently turning untyped. +declare const staleAsset: RiveAsset<{ + artboards: 'Main'; + defaultArtboard: 'Main'; + stateMachines: { Main: 'SM' }; + enums: {}; + viewModels: {}; + referencedAssets: {}; +}>; +expectError(useRiveFile(staleAsset)); +expectError(RiveFileFactory.fromSource(staleAsset, undefined)); diff --git a/src/core/ReferencedAssets.ts b/src/core/ReferencedAssets.ts index 48d11ea5..48033853 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,58 @@ export interface ReferencedAssets { export type ResolvedReferencedAssets = { [assetName: string]: ResolvedReferencedAsset; }; + +type SchemaAssetKeys = Extract< + keyof S['referencedAssets'], + string +>; + +/** Error-message literal: a `never` value would reject entries without saying why. */ +type NoReferencedAssets = Record< + string, + 'this .riv file has no referenced (out-of-band) assets' +>; + +/** Untyped when the schema is unknown, rejecting when the file has no referenced assets. */ +type ForSchemaAssets = + string extends SchemaAssetKeys + ? Untyped + : [SchemaAssetKeys] extends [never] + ? NoReferencedAssets + : Typed; + +/** + * `referencedAssets` constrained to a generated schema: keys must be asset + * `uniqueName`s 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 = ForSchemaAssets< + S, + ReferencedAssets, + { + [K in SchemaAssetKeys]?: S['referencedAssets'][K] extends 'image' + ? (ReferencedAssetSource & { type?: 'image' }) | RiveImage + : ReferencedAssetSource & { + type?: S['referencedAssets'][K] & RiveAssetType; + }; + } +>; + +/** + * Resolved (post-`Image.resolveAssetSource`) form of + * {@link TypedReferencedAssets}, used by the RiveFileFactory methods. + */ +export type TypedResolvedReferencedAssets = + ForSchemaAssets< + S, + ResolvedReferencedAssets, + { + [K in SchemaAssetKeys]?: S['referencedAssets'][K] extends 'image' + ? ResolvedReferencedAsset & { type?: 'image' } + : Omit & { + type?: S['referencedAssets'][K] & RiveAssetType; + }; + } + >; diff --git a/src/core/RiveFile.ts b/src/core/RiveFile.ts index 75e114ae..a75f9c72 100644 --- a/src/core/RiveFile.ts +++ b/src/core/RiveFile.ts @@ -1,9 +1,16 @@ import { NitroModules } from 'react-native-nitro-modules'; import type { RiveFileFactory as RiveFileFactoryInternal } from '../specs/RiveFile.nitro'; -import type { RiveAsset, RiveFileSchema, TypedRiveFile } from './TypedRiveFile'; +import type { + RiveFileSchema, + TypedRiveFile, + SchemaBranded, +} from './TypedRiveFile'; import { Image } from 'react-native'; -import type { ResolvedReferencedAssets } from './ReferencedAssets'; +import type { + ResolvedReferencedAssets, + TypedResolvedReferencedAssets, +} from './ReferencedAssets'; const RiveFileInternal = NitroModules.createHybridObject('RiveFileFactory'); @@ -154,21 +161,17 @@ export namespace RiveFileFactory { * config.resolver.assetExts = [...config.resolver.assetExts, 'riv']; * ``` */ - export async function fromSource( - source: RiveAsset, - referencedAssets: ResolvedReferencedAssets | undefined, - loadCdn?: boolean - ): Promise>; - export async function fromSource( - source: number | { uri: string }, - referencedAssets: ResolvedReferencedAssets | undefined, - loadCdn?: boolean - ): Promise; + // Single generic signature for the same reason as useRiveFile: with a + // typed + untyped overload pair, a bad key on a typed asset falls through to + // the untyped overload instead of erroring. export async function fromSource( - source: number | { uri: string }, - referencedAssets: ResolvedReferencedAssets | undefined, + source: SchemaBranded | { uri: string }, + typedReferencedAssets: TypedResolvedReferencedAssets | undefined, loadCdn?: boolean ): Promise> { + const referencedAssets = typedReferencedAssets as + | ResolvedReferencedAssets + | undefined; const assetID = typeof source === 'number' ? source : null; const sourceURI = typeof source === 'object' ? source.uri : null; diff --git a/src/core/TypedRiveFile.ts b/src/core/TypedRiveFile.ts index e5f169a4..9464b8a0 100644 --- a/src/core/TypedRiveFile.ts +++ b/src/core/TypedRiveFile.ts @@ -9,12 +9,24 @@ import type { RiveFile } from '../specs/RiveFile.nitro'; * signatures make every artboard, view model and enum name valid. */ export interface RiveFileSchema { + /** + * Format version of generated schemas. Bumped on every breaking format + * change, so a `.riv.d.ts` from an older generator is reported as out of + * date instead of silently checking the wrong thing. + */ + schemaVersion: 1; artboards: string; defaultArtboard: string; stateMachines: Record; /** File-level enum definitions, name → value union. */ enums: Record; viewModels: Record>; + /** + * Non-embedded (referenced or hosted) assets that the app must supply via + * the `referencedAssets` option: the asset's runtime `uniqueName` + * (e.g. 'Inter-594377') → 'image' | 'font' | 'audio'. + */ + referencedAssets: Record; } /** @@ -26,9 +38,17 @@ export interface RiveFileSchema { * * Pass to `RiveFileFactory.fromSource(asset)` — TypeScript infers `T` automatically. */ -export type RiveAsset = number & { - readonly __riveSchema?: T; -}; +// An outdated schema resolves to an object keyed by the fix, so the message +// shows up in the compiler error at the use site; `skipLibCheck` hides the +// schema's own mismatch inside the `.d.ts`. +export type RiveAsset = [T] extends [RiveFileSchema] + ? SchemaBranded + : { + readonly 'This .riv.d.ts is out of date: regenerate it with npx rive-gen-types': true; + }; + +/** A Metro asset number carrying schema `T` as a phantom property. */ +export type SchemaBranded = number & { readonly __riveSchema?: T }; /** * Extracts the RiveFileSchema from a RiveAsset, TypedRiveFile, or a bare RiveFileSchema. diff --git a/src/core/TypedViewModelInstance.ts b/src/core/TypedViewModelInstance.ts index a4a576d8..4dc44703 100644 --- a/src/core/TypedViewModelInstance.ts +++ b/src/core/TypedViewModelInstance.ts @@ -230,7 +230,7 @@ export interface TypedViewModelInstance< | undefined; imageProperty( - path: PathsOfKind + path: PathsOfKind ): ViewModelImageProperty | undefined; listProperty( diff --git a/src/hooks/useRiveFile.ts b/src/hooks/useRiveFile.ts index 77449754..ad4df84a 100644 --- a/src/hooks/useRiveFile.ts +++ b/src/hooks/useRiveFile.ts @@ -11,11 +11,12 @@ import type { ReferencedAsset, ReferencedAssets, ResolvedReferencedAssets, + TypedReferencedAssets, } from '../core/ReferencedAssets'; import type { - RiveAsset, RiveFileSchema, TypedRiveFile, + SchemaBranded, } from '../core/TypedRiveFile'; export type { ReferencedAssets, ResolvedReferencedAssets }; @@ -97,17 +98,25 @@ 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 = RiveFileSchema extends T + ? 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: SchemaBranded | 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 ff78476b..d04fb48d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -89,6 +89,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';