From 340a160e5a32b34ea25a9e9c55fdd799684c7bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 29 Jul 2026 20:02:13 +0200 Subject: [PATCH 01/11] feat: generate referenced asset names in .riv schemas, type referencedAssets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an assets record to generated schemas: unique asset identifier (the documented referencedAssets key form, e.g. 'Inter-594377') → 'image' | 'font' | 'audio'. Only non-embedded assets are listed — referenced and CDN-hosted ones are exactly what an app must supply out-of-band on the new runtime, which does not fetch CDN assets. useRiveFile (and RiveFileFactory.fromSource) then constrain referencedAssets for schema-typed inputs: keys must name an asset in the file, each entry's declared type must match the asset's actual kind, and RiveImage objects are only accepted for image assets. useRiveFile's typed/untyped overload pair is merged into one generic signature so an invalid key errors instead of silently falling through to the untyped number-input overload; unknown schemas and plain inputs degrade to the untyped ReferencedAssets as before. Extraction happens in the existing load() asset-loader callback (the only place the WASM exposes asset metadata). The out-of-band example now uses the typed asset import; every mistake class is pinned by tsd tests and the classifyAsset/emit paths by bun tests, with assets added to the emit fuzzer. --- example/assets/rive/GradientBorder.riv.d.ts | 1 + .../rive/arbtboards-models-instances.riv.d.ts | 1 + example/assets/rive/artboard_db_test.riv.d.ts | 1 + example/assets/rive/blinko.riv.d.ts | 1 + example/assets/rive/bouncing_ball.riv.d.ts | 1 + example/assets/rive/click-count.riv.d.ts | 1 + example/assets/rive/counter.riv.d.ts | 1 + example/assets/rive/databinding.riv.d.ts | 1 + .../assets/rive/databinding_images.riv.d.ts | 1 + .../assets/rive/databinding_lists.riv.d.ts | 1 + example/assets/rive/fallback_fonts.riv.d.ts | 1 + example/assets/rive/font_fallback.riv.d.ts | 1 + example/assets/rive/hello_world_text.riv.d.ts | 1 + example/assets/rive/inputglow.riv.d.ts | 1 + .../ios_android_layouts_demo_v01.riv.d.ts | 1 + example/assets/rive/layout_test.riv.d.ts | 1 + example/assets/rive/layouts_demo.riv.d.ts | 1 + example/assets/rive/many_viewmodels.riv.d.ts | 1 + example/assets/rive/movecircle.riv.d.ts | 1 + .../assets/rive/nodefaultbouncing.riv.d.ts | 1 + example/assets/rive/on_entry_test.riv.d.ts | 3 + example/assets/rive/out_of_band.riv.d.ts | 8 +++ example/assets/rive/quick_start.riv.d.ts | 1 + example/assets/rive/rating.riv.d.ts | 1 + example/assets/rive/rewards.riv.d.ts | 1 + .../assets/rive/style_fallback_fonts.riv.d.ts | 1 + example/assets/rive/tabtest.riv.d.ts | 1 + .../assets/rive/viewmodelproperty.riv.d.ts | 1 + .../assets/rive/vm_value_change_test.riv.d.ts | 1 + example/src/exercisers/OutOfBandAssets.tsx | 38 +++++----- expo-example/assets/rive/rating.riv.d.ts | 1 + .../assets/rive/rewards_source.riv.d.ts | 1 + scripts/__tests__/rive-gen-emit.test.ts | 65 ++++++++++++++++- scripts/__tests__/rive-gen-fuzz.test.ts | Bin 5321 -> 5553 bytes scripts/rive-extract-schema.ts | 17 ++++- scripts/rive-gen-types.ts | 67 ++++++++++++++++-- src/__tests__/typed-rive.test-d.ts | 59 +++++++++++++++ src/core/ReferencedAssets.ts | 39 ++++++++++ src/core/RiveFile.ts | 7 +- src/core/TypedRiveFile.ts | 6 ++ src/hooks/useRiveFile.ts | 32 ++++++--- src/index.tsx | 4 ++ 42 files changed, 335 insertions(+), 39 deletions(-) diff --git a/example/assets/rive/GradientBorder.riv.d.ts b/example/assets/rive/GradientBorder.riv.d.ts index 8e9d41ec..7839f39a 100644 --- a/example/assets/rive/GradientBorder.riv.d.ts +++ b/example/assets/rive/GradientBorder.riv.d.ts @@ -14,5 +14,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 225a4f90..5f8815de 100644 --- a/example/assets/rive/arbtboards-models-instances.riv.d.ts +++ b/example/assets/rive/arbtboards-models-instances.riv.d.ts @@ -23,5 +23,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 4c3eb1e0..8c784ff3 100644 --- a/example/assets/rive/artboard_db_test.riv.d.ts +++ b/example/assets/rive/artboard_db_test.riv.d.ts @@ -18,5 +18,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 bc077aca..6d28f44e 100644 --- a/example/assets/rive/blinko.riv.d.ts +++ b/example/assets/rive/blinko.riv.d.ts @@ -97,5 +97,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 7e193b35..23eefa61 100644 --- a/example/assets/rive/bouncing_ball.riv.d.ts +++ b/example/assets/rive/bouncing_ball.riv.d.ts @@ -14,5 +14,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 b7f43555..e504a748 100644 --- a/example/assets/rive/click-count.riv.d.ts +++ b/example/assets/rive/click-count.riv.d.ts @@ -14,5 +14,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 82927ffe..efbf150b 100644 --- a/example/assets/rive/counter.riv.d.ts +++ b/example/assets/rive/counter.riv.d.ts @@ -14,5 +14,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 f3aae26d..4012588a 100644 --- a/example/assets/rive/databinding.riv.d.ts +++ b/example/assets/rive/databinding.riv.d.ts @@ -27,5 +27,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 1ecb298a..7f013b74 100644 --- a/example/assets/rive/databinding_images.riv.d.ts +++ b/example/assets/rive/databinding_images.riv.d.ts @@ -14,5 +14,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 b11c0ec1..e2078244 100644 --- a/example/assets/rive/databinding_lists.riv.d.ts +++ b/example/assets/rive/databinding_lists.riv.d.ts @@ -17,5 +17,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 dfbf4cb5..13a8c91c 100644 --- a/example/assets/rive/fallback_fonts.riv.d.ts +++ b/example/assets/rive/fallback_fonts.riv.d.ts @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; 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 c7df2170..937a0b6e 100644 --- a/example/assets/rive/font_fallback.riv.d.ts +++ b/example/assets/rive/font_fallback.riv.d.ts @@ -14,5 +14,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 2d6a1e5a..9fc02caf 100644 --- a/example/assets/rive/hello_world_text.riv.d.ts +++ b/example/assets/rive/hello_world_text.riv.d.ts @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + assets: {}; }>; export default asset; diff --git a/example/assets/rive/inputglow.riv.d.ts b/example/assets/rive/inputglow.riv.d.ts index 12ce5ed9..91747474 100644 --- a/example/assets/rive/inputglow.riv.d.ts +++ b/example/assets/rive/inputglow.riv.d.ts @@ -14,5 +14,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 3e3bc792..b58a3be5 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 @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; 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 dc5b67de..af56e7e2 100644 --- a/example/assets/rive/layout_test.riv.d.ts +++ b/example/assets/rive/layout_test.riv.d.ts @@ -14,5 +14,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 9443f4e1..04c06ec5 100644 --- a/example/assets/rive/layouts_demo.riv.d.ts +++ b/example/assets/rive/layouts_demo.riv.d.ts @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; 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 bf0191f9..741ebb0a 100644 --- a/example/assets/rive/many_viewmodels.riv.d.ts +++ b/example/assets/rive/many_viewmodels.riv.d.ts @@ -16,5 +16,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 b156f66e..ddde33c0 100644 --- a/example/assets/rive/movecircle.riv.d.ts +++ b/example/assets/rive/movecircle.riv.d.ts @@ -14,5 +14,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 63856505..0bb23df1 100644 --- a/example/assets/rive/nodefaultbouncing.riv.d.ts +++ b/example/assets/rive/nodefaultbouncing.riv.d.ts @@ -14,5 +14,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 93f933ea..aa0e671c 100644 --- a/example/assets/rive/on_entry_test.riv.d.ts +++ b/example/assets/rive/on_entry_test.riv.d.ts @@ -15,5 +15,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 766bc851..0a9f2312 100644 --- a/example/assets/rive/out_of_band.riv.d.ts +++ b/example/assets/rive/out_of_band.riv.d.ts @@ -10,5 +10,13 @@ declare const asset: RiveAsset<{ }; enums: {}; 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 ac069c38..3beb2b15 100644 --- a/example/assets/rive/quick_start.riv.d.ts +++ b/example/assets/rive/quick_start.riv.d.ts @@ -18,5 +18,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 7b7c9c4a..ab0688ec 100644 --- a/example/assets/rive/rating.riv.d.ts +++ b/example/assets/rive/rating.riv.d.ts @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + assets: {}; }>; export default asset; diff --git a/example/assets/rive/rewards.riv.d.ts b/example/assets/rive/rewards.riv.d.ts index c90a9cfa..76956ce1 100644 --- a/example/assets/rive/rewards.riv.d.ts +++ b/example/assets/rive/rewards.riv.d.ts @@ -50,5 +50,6 @@ declare const asset: RiveAsset<{ Item_Selection: 'enum:Item_Selection'; }; }; + 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 fbffcf2e..af08a939 100644 --- a/example/assets/rive/style_fallback_fonts.riv.d.ts +++ b/example/assets/rive/style_fallback_fonts.riv.d.ts @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; + assets: {}; }>; export default asset; diff --git a/example/assets/rive/tabtest.riv.d.ts b/example/assets/rive/tabtest.riv.d.ts index e2a706d9..5fe9bf8e 100644 --- a/example/assets/rive/tabtest.riv.d.ts +++ b/example/assets/rive/tabtest.riv.d.ts @@ -16,5 +16,6 @@ declare const asset: RiveAsset<{ enumProperty: 'enum:Selected'; }; }; + assets: {}; }>; export default asset; diff --git a/example/assets/rive/viewmodelproperty.riv.d.ts b/example/assets/rive/viewmodelproperty.riv.d.ts index e7398f8d..629fbdc3 100644 --- a/example/assets/rive/viewmodelproperty.riv.d.ts +++ b/example/assets/rive/viewmodelproperty.riv.d.ts @@ -18,5 +18,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 62ee2ff1..890971f8 100644 --- a/example/assets/rive/vm_value_change_test.riv.d.ts +++ b/example/assets/rive/vm_value_change_test.riv.d.ts @@ -15,5 +15,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 7b7c9c4a..ab0688ec 100644 --- a/expo-example/assets/rive/rating.riv.d.ts +++ b/expo-example/assets/rive/rating.riv.d.ts @@ -10,5 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; 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 ae121d9c..c7945c94 100644 --- a/expo-example/assets/rive/rewards_source.riv.d.ts +++ b/expo-example/assets/rive/rewards_source.riv.d.ts @@ -50,5 +50,6 @@ declare const asset: RiveAsset<{ Item_Selection: 'enum:Item_Selection'; }; }; + assets: {}; }>; export default asset; diff --git a/scripts/__tests__/rive-gen-emit.test.ts b/scripts/__tests__/rive-gen-emit.test.ts index 6920218d..dc7aab97 100644 --- a/scripts/__tests__/rive-gen-emit.test.ts +++ b/scripts/__tests__/rive-gen-emit.test.ts @@ -7,6 +7,8 @@ import { unionRecord, vmRecord, schemaBody, + assetsRecord, + classifyAsset, enumPropTypeString, collectEnums, viewModelRefTypeString, @@ -50,6 +52,7 @@ describe('emit escaping', () => { viewModels: { "It's VM": { "quote'": "enum:Pet's" }, }, + assets: { "Font's-123": 'font' }, }; const body = schemaBody(schema); expect(parseErrors(`declare const asset: {\n${body}\n};`)).toEqual([]); @@ -84,10 +87,21 @@ describe('schemaBody', () => { stateMachines: { Main: ['SM'] }, enums: {}, 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('always emits enums, empty object when none', () => { @@ -193,3 +207,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 fba72d08a340f4c18df96e1b380ab34fc848542a..2033de6773939aca60afabb0f2c6547f70561c55 100644 GIT binary patch delta 142 zcmX@9xlwzAFBfleeqM2jLSk`oYDuxxWCITI$*VayC!4dIPyWmy84eMTE=tTx$ = {}; 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 +136,14 @@ async function main() { const defaultArtboard = artboards[0] ?? ''; process.stdout.write( JSON.stringify( - { artboards, defaultArtboard, stateMachines, enums, viewModels }, + { + artboards, + defaultArtboard, + stateMachines, + enums, + viewModels, + assets, + }, null, 2 ) + '\n' diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index c9707978..15789018 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -49,6 +49,45 @@ export interface Schema { /** File-level enum definitions, name → values. */ enums: 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; } export interface RuntimeProperty { @@ -103,8 +142,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; @@ -162,6 +208,7 @@ async function extractSchema(input: string): Promise { stateMachines, enums, viewModels, + assets, }; } @@ -209,23 +256,35 @@ 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 { 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/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: { ${unionRecord(schema.stateMachines)} - };${enumSection}${vmSection}`; + };${enumSection}${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 807d251a..37260561 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -26,7 +26,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'; @@ -495,3 +498,59 @@ 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) +useRiveFile('https://example.com/a.riv', { + referencedAssets: { anything: { source: 1, type: 'font' } }, +}); +useRiveFile({ uri: 'file:///a.riv' }, { + referencedAssets: { whatever: { source: 1 } }, +}); diff --git a/src/core/ReferencedAssets.ts b/src/core/ReferencedAssets.ts index 48d11ea5..4c5d0831 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,41 @@ 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; `never` values reject every entry. + 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 e5f169a4..f69eab1a 100644 --- a/src/core/TypedRiveFile.ts +++ b/src/core/TypedRiveFile.ts @@ -15,6 +15,12 @@ export interface RiveFileSchema { /** File-level enum definitions, name → value union. */ enums: 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 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'; From 7da63ac748291d681e52176254ccb2215b415e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 30 Jul 2026 06:45:05 +0200 Subject: [PATCH 02/11] fix(types): explain the no-referenced-assets rejection, pin all untyped input forms A referencedAssets entry on a file with no referenced assets errored with an unexplained 'not assignable to never'; the rejection value is now a literal message type so the compiler error itself says why. Also pin that every legacy input form (url string, uri object, require()/any, plain number) keeps accepting arbitrary asset keys. --- src/__tests__/typed-rive.test-d.ts | 11 ++++++++++- src/core/ReferencedAssets.ts | 11 +++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts index 37260561..f6fc1e36 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -547,10 +547,19 @@ expectError( }) ); -// Untyped inputs keep accepting arbitrary keys (backward compat) +// 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 4c5d0831..b9bb763b 100644 --- a/src/core/ReferencedAssets.ts +++ b/src/core/ReferencedAssets.ts @@ -37,9 +37,12 @@ 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; `never` values reject every entry. - Record + ? // 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'], @@ -59,7 +62,7 @@ export type TypedResolvedReferencedAssets = string extends Extract ? ResolvedReferencedAssets : [Extract] extends [never] - ? Record + ? Record : { [K in Extract]?: ResolvedReferencedAsset; }; From ffac1530f4ec63b4b40b2cfbcc585853d5218000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 12 Aug 2026 06:00:36 +0200 Subject: [PATCH 03/11] test: assert out-of-band asset extraction end to end Pins the full assets record extracted from out_of_band.riv (3 referenced + 3 CDN, embedded excluded) and that embedded-only files report no assets. --- scripts/__tests__/rive-extract-schema.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/__tests__/rive-extract-schema.test.ts b/scripts/__tests__/rive-extract-schema.test.ts index d9aec246..d6705824 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 with unique ids and kinds', () => { + const oob = extract( + resolve(import.meta.dirname, '../../example/assets/rive/out_of_band.riv') + ); + expect(oob.assets).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.assets).toEqual({}); + }); + test('enum properties reference a file-level enum definition', () => { const databinding = extract( resolve(import.meta.dirname, '../../example/assets/rive/databinding.riv') From f966829fe373524166ab364cffc01a692bd89e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 23 Sep 2026 06:52:15 +0200 Subject: [PATCH 04/11] test: give hand-written schemas the assets key, regenerate finished_trigger schema RiveFileSchema requires assets since referenced-asset typing; the enum fixtures predate it, and finished_trigger.riv.d.ts was generated before the assets section existed. --- example/assets/rive/finished_trigger.riv.d.ts | 1 + src/__tests__/typed-rive.test-d.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/example/assets/rive/finished_trigger.riv.d.ts b/example/assets/rive/finished_trigger.riv.d.ts index b1b61298..29aa121f 100644 --- a/example/assets/rive/finished_trigger.riv.d.ts +++ b/example/assets/rive/finished_trigger.riv.d.ts @@ -14,5 +14,6 @@ declare const asset: RiveAsset<{ finished: 'trigger'; }; }; + assets: {}; }>; export default asset; diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts index f6fc1e36..a0edf0d9 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -270,6 +270,7 @@ type BuiltInEnumSchema = { stateMachines: { Main: 'SM' }; enums: {}; viewModels: { VM: { blend: 'enum' } }; + assets: {}; }; declare const builtInVM: TypedViewModelInstance; expectType>(useRiveEnum('blend', builtInVM)); @@ -279,6 +280,7 @@ type DanglingSchema = { artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; + assets: {}; enums: {}; viewModels: { VM: { pet: 'enum:Pets' } }; }; @@ -289,6 +291,7 @@ type EmptyEnumSchema = { artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; + assets: {}; enums: { E: never }; viewModels: { VM: { pet: 'enum:E' } }; }; From 2467f5c990347237c9d28d927df536ba47422625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 23 Sep 2026 07:00:19 +0200 Subject: [PATCH 05/11] fix(types): check referencedAssets in fromSource, keep hand-written schemas typed RiveFileFactory.fromSource kept a typed + untyped overload pair, so a misspelled asset key, a wrong declared type, or a RiveImage for a font on a schema-typed asset fell through to the untyped overload and compiled. It now has a single generic signature like useRiveFile, and the resolved entries are kind-checked the same way as the hook's. Both typings share one schema switch. useRiveFile decided typed vs untyped by string artboards, so a standalone hand-written schema with literal ViewModels lost its typing; it now checks for the base schema itself. --- src/__tests__/typed-rive.test-d.ts | 50 ++++++++++++++++++++++ src/core/ReferencedAssets.ts | 66 ++++++++++++++++++------------ src/core/RiveFile.ts | 20 ++++----- src/hooks/useRiveFile.ts | 13 +++--- 4 files changed, 103 insertions(+), 46 deletions(-) diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts index a0edf0d9..c4759d6d 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -27,6 +27,7 @@ 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'; @@ -566,3 +567,52 @@ 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 = { + artboards: string; + defaultArtboard: string; + stateMachines: Record; + enums: {}; + assets: {}; + viewModels: { VM: { n: 'number' } }; +}; +declare const handWrittenAsset: RiveAsset; +{ + const { riveFile } = useRiveFile(handWrittenAsset); + expectError( + useViewModelInstance(riveFile, { viewModelName: 'NotAVM', async: true }) + ); +} diff --git a/src/core/ReferencedAssets.ts b/src/core/ReferencedAssets.ts index b9bb763b..5fe395bf 100644 --- a/src/core/ReferencedAssets.ts +++ b/src/core/ReferencedAssets.ts @@ -26,6 +26,25 @@ export type ResolvedReferencedAssets = { [assetName: string]: ResolvedReferencedAsset; }; +type SchemaAssetKeys = Extract< + keyof S['assets'], + 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 * unique identifiers from the .riv file, and each entry's declared `type` @@ -33,36 +52,29 @@ export type ResolvedReferencedAssets = { * 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; - }; - }; +export type TypedReferencedAssets = ForSchemaAssets< + S, + ReferencedAssets, + { + [K in SchemaAssetKeys]?: 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; - }; + ForSchemaAssets< + S, + ResolvedReferencedAssets, + { + [K in SchemaAssetKeys]?: S['assets'][K] extends 'image' + ? ResolvedReferencedAsset & { type?: 'image' } + : Omit & { + type?: S['assets'][K] & RiveAssetType; + }; + } + >; diff --git a/src/core/RiveFile.ts b/src/core/RiveFile.ts index c4596747..51259df8 100644 --- a/src/core/RiveFile.ts +++ b/src/core/RiveFile.ts @@ -157,21 +157,17 @@ export namespace RiveFileFactory { * config.resolver.assetExts = [...config.resolver.assetExts, 'riv']; * ``` */ - export async function fromSource( - source: RiveAsset, - referencedAssets: TypedResolvedReferencedAssets | 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: RiveAsset | { 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/hooks/useRiveFile.ts b/src/hooks/useRiveFile.ts index 78b2690e..b993f158 100644 --- a/src/hooks/useRiveFile.ts +++ b/src/hooks/useRiveFile.ts @@ -102,13 +102,12 @@ export type 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 }; +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 From 885a7d22947dc32b3276b76d79f06e8f04e03684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 23 Sep 2026 07:00:19 +0200 Subject: [PATCH 06/11] refactor(scripts): share the asset-collecting loader between both scripts --- scripts/rive-extract-schema.ts | 11 ++--------- scripts/rive-gen-types.ts | 31 +++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/scripts/rive-extract-schema.ts b/scripts/rive-extract-schema.ts index e2469ad8..ae3190bd 100644 --- a/scripts/rive-extract-schema.ts +++ b/scripts/rive-extract-schema.ts @@ -14,8 +14,8 @@ import { readFileSync } from 'fs'; import riveCanvas from '@rive-app/canvas'; const { RuntimeLoader } = riveCanvas; import { - classifyAsset, collectEnums, + createAssetCollector, enumPropTypeString, nameMap, viewModelRefTypeString, @@ -74,14 +74,7 @@ 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: (asset: any, embeddedBytes: Uint8Array | undefined) => { - const classified = classifyAsset(asset ?? {}, embeddedBytes?.length ?? 0); - if (classified) assets[classified.id] = classified.kind; - return true; - }, - }); + const { assetLoader, assets } = createAssetCollector(runtime); let timer: ReturnType | undefined; const riveFile = await Promise.race([ diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index 15789018..e2561a98 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -142,16 +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. - // 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: (asset: any, embeddedBytes: Uint8Array | undefined) => { - const classified = classifyAsset(asset ?? {}, embeddedBytes?.length ?? 0); - if (classified) assets[classified.id] = classified.kind; - return true; - }, - }); + const { assetLoader, assets } = createAssetCollector(runtime); let timer: ReturnType | undefined; const riveFile = await Promise.race([ @@ -256,6 +247,26 @@ 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: Record = {}; + 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); From 5ff93f67a07be65231b02c19f8261f3081ba45fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 23 Sep 2026 11:27:05 +0200 Subject: [PATCH 07/11] refactor(schema): name the asset section referencedAssets It lists only non-embedded (referenced or hosted) assets, exactly what the referencedAssets option it types accepts. Asset keys are described as the runtime's uniqueName. --- example/assets/rive/GradientBorder.riv.d.ts | 2 +- .../rive/arbtboards-models-instances.riv.d.ts | 2 +- example/assets/rive/artboard_db_test.riv.d.ts | 2 +- example/assets/rive/blinko.riv.d.ts | 2 +- example/assets/rive/bouncing_ball.riv.d.ts | 2 +- example/assets/rive/click-count.riv.d.ts | 2 +- example/assets/rive/counter.riv.d.ts | 2 +- example/assets/rive/databinding.riv.d.ts | 2 +- .../assets/rive/databinding_images.riv.d.ts | 2 +- .../assets/rive/databinding_lists.riv.d.ts | 2 +- example/assets/rive/fallback_fonts.riv.d.ts | 2 +- example/assets/rive/finished_trigger.riv.d.ts | 2 +- example/assets/rive/font_fallback.riv.d.ts | 2 +- example/assets/rive/hello_world_text.riv.d.ts | 2 +- example/assets/rive/inputglow.riv.d.ts | 2 +- .../ios_android_layouts_demo_v01.riv.d.ts | 2 +- example/assets/rive/layout_test.riv.d.ts | 2 +- example/assets/rive/layouts_demo.riv.d.ts | 2 +- example/assets/rive/many_viewmodels.riv.d.ts | 2 +- example/assets/rive/movecircle.riv.d.ts | 2 +- .../assets/rive/nodefaultbouncing.riv.d.ts | 2 +- example/assets/rive/on_entry_test.riv.d.ts | 2 +- example/assets/rive/out_of_band.riv.d.ts | 2 +- example/assets/rive/quick_start.riv.d.ts | 2 +- example/assets/rive/rating.riv.d.ts | 2 +- example/assets/rive/rewards.riv.d.ts | 2 +- .../assets/rive/style_fallback_fonts.riv.d.ts | 2 +- example/assets/rive/tabtest.riv.d.ts | 2 +- .../assets/rive/viewmodelproperty.riv.d.ts | 2 +- .../assets/rive/vm_value_change_test.riv.d.ts | 2 +- expo-example/assets/rive/rating.riv.d.ts | 2 +- .../assets/rive/rewards_source.riv.d.ts | 2 +- scripts/__tests__/rive-extract-schema.test.ts | 6 ++--- scripts/__tests__/rive-gen-emit.test.ts | 12 ++++----- scripts/__tests__/rive-gen-fuzz.test.ts | Bin 5553 -> 5571 bytes scripts/rive-extract-schema.ts | 2 +- scripts/rive-gen-types.ts | 24 +++++++++--------- src/__tests__/typed-rive.test-d.ts | 8 +++--- src/core/ReferencedAssets.ts | 14 +++++----- src/core/TypedRiveFile.ts | 8 +++--- 40 files changed, 70 insertions(+), 68 deletions(-) diff --git a/example/assets/rive/GradientBorder.riv.d.ts b/example/assets/rive/GradientBorder.riv.d.ts index 7839f39a..ac3a0306 100644 --- a/example/assets/rive/GradientBorder.riv.d.ts +++ b/example/assets/rive/GradientBorder.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ isFocused: 'boolean'; }; }; - assets: {}; + 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 5f8815de..42ffd90d 100644 --- a/example/assets/rive/arbtboards-models-instances.riv.d.ts +++ b/example/assets/rive/arbtboards-models-instances.riv.d.ts @@ -23,6 +23,6 @@ declare const asset: RiveAsset<{ _id: 'string'; }; }; - assets: {}; + 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 8c784ff3..ed172f33 100644 --- a/example/assets/rive/artboard_db_test.riv.d.ts +++ b/example/assets/rive/artboard_db_test.riv.d.ts @@ -18,6 +18,6 @@ declare const asset: RiveAsset<{ artboard_2: 'artboard'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/blinko.riv.d.ts b/example/assets/rive/blinko.riv.d.ts index 6d28f44e..9a51eddd 100644 --- a/example/assets/rive/blinko.riv.d.ts +++ b/example/assets/rive/blinko.riv.d.ts @@ -97,6 +97,6 @@ declare const asset: RiveAsset<{ 'property of gameLogicVM': 'viewModel:gameLogicVM'; }; }; - assets: {}; + 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 23eefa61..3cfed0d9 100644 --- a/example/assets/rive/bouncing_ball.riv.d.ts +++ b/example/assets/rive/bouncing_ball.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ ypos: 'number'; }; }; - assets: {}; + 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 e504a748..82e2d7ce 100644 --- a/example/assets/rive/click-count.riv.d.ts +++ b/example/assets/rive/click-count.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ clickCount: 'number'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/counter.riv.d.ts b/example/assets/rive/counter.riv.d.ts index efbf150b..0b5660bd 100644 --- a/example/assets/rive/counter.riv.d.ts +++ b/example/assets/rive/counter.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ cnt: 'number'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/databinding.riv.d.ts b/example/assets/rive/databinding.riv.d.ts index 4012588a..f14771dd 100644 --- a/example/assets/rive/databinding.riv.d.ts +++ b/example/assets/rive/databinding.riv.d.ts @@ -27,6 +27,6 @@ declare const asset: RiveAsset<{ name: 'string'; }; }; - assets: {}; + 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 7f013b74..e580a0d4 100644 --- a/example/assets/rive/databinding_images.riv.d.ts +++ b/example/assets/rive/databinding_images.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ bound_image: 'image'; }; }; - assets: {}; + 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 e2078244..77711784 100644 --- a/example/assets/rive/databinding_lists.riv.d.ts +++ b/example/assets/rive/databinding_lists.riv.d.ts @@ -17,6 +17,6 @@ declare const asset: RiveAsset<{ team: 'list'; }; }; - assets: {}; + 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 13a8c91c..20a1d948 100644 --- a/example/assets/rive/fallback_fonts.riv.d.ts +++ b/example/assets/rive/fallback_fonts.riv.d.ts @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + 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 29aa121f..838a398c 100644 --- a/example/assets/rive/finished_trigger.riv.d.ts +++ b/example/assets/rive/finished_trigger.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ finished: 'trigger'; }; }; - assets: {}; + 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 937a0b6e..11c9f814 100644 --- a/example/assets/rive/font_fallback.riv.d.ts +++ b/example/assets/rive/font_fallback.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ text: 'string'; }; }; - assets: {}; + 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 9fc02caf..56b24a8e 100644 --- a/example/assets/rive/hello_world_text.riv.d.ts +++ b/example/assets/rive/hello_world_text.riv.d.ts @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/inputglow.riv.d.ts b/example/assets/rive/inputglow.riv.d.ts index 91747474..f7456ff3 100644 --- a/example/assets/rive/inputglow.riv.d.ts +++ b/example/assets/rive/inputglow.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ isFocused: 'boolean'; }; }; - assets: {}; + 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 b58a3be5..19f1e125 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 @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + 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 af56e7e2..80b1075b 100644 --- a/example/assets/rive/layout_test.riv.d.ts +++ b/example/assets/rive/layout_test.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ }; }; - assets: {}; + 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 04c06ec5..65285d57 100644 --- a/example/assets/rive/layouts_demo.riv.d.ts +++ b/example/assets/rive/layouts_demo.riv.d.ts @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + 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 741ebb0a..4673f98c 100644 --- a/example/assets/rive/many_viewmodels.riv.d.ts +++ b/example/assets/rive/many_viewmodels.riv.d.ts @@ -16,6 +16,6 @@ declare const asset: RiveAsset<{ textValue: 'string'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/movecircle.riv.d.ts b/example/assets/rive/movecircle.riv.d.ts index ddde33c0..aee98b60 100644 --- a/example/assets/rive/movecircle.riv.d.ts +++ b/example/assets/rive/movecircle.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ posY: 'number'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/nodefaultbouncing.riv.d.ts b/example/assets/rive/nodefaultbouncing.riv.d.ts index 0bb23df1..6405947d 100644 --- a/example/assets/rive/nodefaultbouncing.riv.d.ts +++ b/example/assets/rive/nodefaultbouncing.riv.d.ts @@ -14,6 +14,6 @@ declare const asset: RiveAsset<{ }; }; - assets: {}; + 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 aa0e671c..5f97bd05 100644 --- a/example/assets/rive/on_entry_test.riv.d.ts +++ b/example/assets/rive/on_entry_test.riv.d.ts @@ -15,7 +15,7 @@ declare const asset: RiveAsset<{ inputOnEntry: 'number'; }; }; - assets: { + referencedAssets: { 'Inter-4228759': 'font'; }; }>; diff --git a/example/assets/rive/out_of_band.riv.d.ts b/example/assets/rive/out_of_band.riv.d.ts index 0a9f2312..54504bea 100644 --- a/example/assets/rive/out_of_band.riv.d.ts +++ b/example/assets/rive/out_of_band.riv.d.ts @@ -10,7 +10,7 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: { + referencedAssets: { 'Inter-594377': 'font'; 'referenced-image-2929282': 'image'; 'referenced_audio-2929340': 'audio'; diff --git a/example/assets/rive/quick_start.riv.d.ts b/example/assets/rive/quick_start.riv.d.ts index 3beb2b15..f7103a8c 100644 --- a/example/assets/rive/quick_start.riv.d.ts +++ b/example/assets/rive/quick_start.riv.d.ts @@ -18,6 +18,6 @@ declare const asset: RiveAsset<{ health: 'number'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/rating.riv.d.ts b/example/assets/rive/rating.riv.d.ts index ab0688ec..923c4d1a 100644 --- a/example/assets/rive/rating.riv.d.ts +++ b/example/assets/rive/rating.riv.d.ts @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/rewards.riv.d.ts b/example/assets/rive/rewards.riv.d.ts index 76956ce1..5d446579 100644 --- a/example/assets/rive/rewards.riv.d.ts +++ b/example/assets/rive/rewards.riv.d.ts @@ -50,6 +50,6 @@ declare const asset: RiveAsset<{ Item_Selection: 'enum:Item_Selection'; }; }; - assets: {}; + 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 af08a939..92e1821e 100644 --- a/example/assets/rive/style_fallback_fonts.riv.d.ts +++ b/example/assets/rive/style_fallback_fonts.riv.d.ts @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/tabtest.riv.d.ts b/example/assets/rive/tabtest.riv.d.ts index 5fe9bf8e..23a3b845 100644 --- a/example/assets/rive/tabtest.riv.d.ts +++ b/example/assets/rive/tabtest.riv.d.ts @@ -16,6 +16,6 @@ declare const asset: RiveAsset<{ enumProperty: 'enum:Selected'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/example/assets/rive/viewmodelproperty.riv.d.ts b/example/assets/rive/viewmodelproperty.riv.d.ts index 629fbdc3..c967d944 100644 --- a/example/assets/rive/viewmodelproperty.riv.d.ts +++ b/example/assets/rive/viewmodelproperty.riv.d.ts @@ -18,6 +18,6 @@ declare const asset: RiveAsset<{ name: 'string'; }; }; - assets: {}; + 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 890971f8..58bfad25 100644 --- a/example/assets/rive/vm_value_change_test.riv.d.ts +++ b/example/assets/rive/vm_value_change_test.riv.d.ts @@ -15,6 +15,6 @@ declare const asset: RiveAsset<{ textValue: 'string'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/expo-example/assets/rive/rating.riv.d.ts b/expo-example/assets/rive/rating.riv.d.ts index ab0688ec..923c4d1a 100644 --- a/expo-example/assets/rive/rating.riv.d.ts +++ b/expo-example/assets/rive/rating.riv.d.ts @@ -10,6 +10,6 @@ declare const asset: RiveAsset<{ }; enums: {}; viewModels: {}; - assets: {}; + 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 c7945c94..a1a0b91d 100644 --- a/expo-example/assets/rive/rewards_source.riv.d.ts +++ b/expo-example/assets/rive/rewards_source.riv.d.ts @@ -50,6 +50,6 @@ declare const asset: RiveAsset<{ Item_Selection: 'enum:Item_Selection'; }; }; - assets: {}; + referencedAssets: {}; }>; export default asset; diff --git a/scripts/__tests__/rive-extract-schema.test.ts b/scripts/__tests__/rive-extract-schema.test.ts index d6705824..2c20659b 100644 --- a/scripts/__tests__/rive-extract-schema.test.ts +++ b/scripts/__tests__/rive-extract-schema.test.ts @@ -54,11 +54,11 @@ describe('rive-extract-schema', () => { expect(vms.Energy_Bar!.Bar_Color).toBe('color'); }); - test('extracts non-embedded assets with unique ids and kinds', () => { + 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.assets).toEqual({ + expect(oob.referencedAssets).toEqual({ 'Inter-594377': 'font', 'referenced-image-2929282': 'image', 'referenced_audio-2929340': 'audio', @@ -69,7 +69,7 @@ describe('rive-extract-schema', () => { }); test('embedded-only files report no referenced assets', () => { - expect(schema.assets).toEqual({}); + expect(schema.referencedAssets).toEqual({}); }); test('enum properties reference a file-level enum definition', () => { diff --git a/scripts/__tests__/rive-gen-emit.test.ts b/scripts/__tests__/rive-gen-emit.test.ts index dc7aab97..bc861e20 100644 --- a/scripts/__tests__/rive-gen-emit.test.ts +++ b/scripts/__tests__/rive-gen-emit.test.ts @@ -52,7 +52,7 @@ describe('emit escaping', () => { viewModels: { "It's VM": { "quote'": "enum:Pet's" }, }, - assets: { "Font's-123": 'font' }, + referencedAssets: { "Font's-123": 'font' }, }; const body = schemaBody(schema); expect(parseErrors(`declare const asset: {\n${body}\n};`)).toEqual([]); @@ -87,18 +87,18 @@ describe('schemaBody', () => { stateMachines: { Main: ['SM'] }, enums: {}, viewModels: {}, - assets: {}, + referencedAssets: {}, }; - test('always emits viewModels and assets, empty objects when none', () => { + test('always emits viewModels and referencedAssets, empty objects when none', () => { expect(schemaBody(base)).toContain('viewModels: {};'); - expect(schemaBody(base)).toContain('assets: {};'); + expect(schemaBody(base)).toContain('referencedAssets: {};'); }); - test('emits assets record when present', () => { + test('emits referencedAssets record when present', () => { const body = schemaBody({ ...base, - assets: { 'Inter-594377': 'font', 'img-1': 'image' }, + referencedAssets: { 'Inter-594377': 'font', 'img-1': 'image' }, }); expect(body).toContain("'Inter-594377': 'font';"); expect(body).toContain("'img-1': 'image';"); diff --git a/scripts/__tests__/rive-gen-fuzz.test.ts b/scripts/__tests__/rive-gen-fuzz.test.ts index 2033de6773939aca60afabb0f2c6547f70561c55..caee15ed426c965556b748e6f09e338c15aa60f2 100644 GIT binary patch delta 30 lcmdm}eOP6vyJ?)RJPW&C|H0xd6Pg3ugcT delta 12 TcmX@Cy-|C^bneY7xuv)OBijV1 diff --git a/scripts/rive-extract-schema.ts b/scripts/rive-extract-schema.ts index ae3190bd..9e064acc 100644 --- a/scripts/rive-extract-schema.ts +++ b/scripts/rive-extract-schema.ts @@ -135,7 +135,7 @@ async function main() { stateMachines, enums, viewModels, - assets, + referencedAssets: assets, }, null, 2 diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index e2561a98..5b406321 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -49,17 +49,17 @@ export interface Schema { /** File-level enum definitions, name → values. */ enums: Record; viewModels: Record>; - /** Non-embedded (referenced or CDN) assets: unique identifier → kind. */ - assets: 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 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. + * 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: { @@ -199,7 +199,7 @@ async function extractSchema(input: string): Promise { stateMachines, enums, viewModels, - assets, + referencedAssets: assets, }; } @@ -280,16 +280,16 @@ export function schemaBody(schema: Schema): string { Object.keys(schema.enums).length > 0 ? `\n enums: {\n${unionRecord(schema.enums)}\n };` : '\n enums: {};'; - // Always emit viewModels/assets — omitting either would fail the + // Always emit viewModels/referencedAssets — 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: {};'; + Object.keys(schema.referencedAssets).length > 0 + ? `\n referencedAssets: {\n${assetsRecord(schema.referencedAssets)}\n };` + : '\n referencedAssets: {};'; return `\ artboards: ${schema.artboards.map(strLit).join(' | ')}; defaultArtboard: ${strLit(schema.defaultArtboard)}; diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts index c4759d6d..ac4d4889 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -271,7 +271,7 @@ type BuiltInEnumSchema = { stateMachines: { Main: 'SM' }; enums: {}; viewModels: { VM: { blend: 'enum' } }; - assets: {}; + referencedAssets: {}; }; declare const builtInVM: TypedViewModelInstance; expectType>(useRiveEnum('blend', builtInVM)); @@ -281,7 +281,7 @@ type DanglingSchema = { artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; - assets: {}; + referencedAssets: {}; enums: {}; viewModels: { VM: { pet: 'enum:Pets' } }; }; @@ -292,7 +292,7 @@ type EmptyEnumSchema = { artboards: 'Main'; defaultArtboard: 'Main'; stateMachines: { Main: 'SM' }; - assets: {}; + referencedAssets: {}; enums: { E: never }; viewModels: { VM: { pet: 'enum:E' } }; }; @@ -606,7 +606,7 @@ type HandWrittenSchema = { defaultArtboard: string; stateMachines: Record; enums: {}; - assets: {}; + referencedAssets: {}; viewModels: { VM: { n: 'number' } }; }; declare const handWrittenAsset: RiveAsset; diff --git a/src/core/ReferencedAssets.ts b/src/core/ReferencedAssets.ts index 5fe395bf..48033853 100644 --- a/src/core/ReferencedAssets.ts +++ b/src/core/ReferencedAssets.ts @@ -27,7 +27,7 @@ export type ResolvedReferencedAssets = { }; type SchemaAssetKeys = Extract< - keyof S['assets'], + keyof S['referencedAssets'], string >; @@ -47,7 +47,7 @@ type ForSchemaAssets = /** * `referencedAssets` constrained to a generated schema: keys must be asset - * unique identifiers from the .riv file, and each entry's declared `type` + * `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. @@ -56,9 +56,11 @@ export type TypedReferencedAssets = ForSchemaAssets< S, ReferencedAssets, { - [K in SchemaAssetKeys]?: S['assets'][K] extends 'image' + [K in SchemaAssetKeys]?: S['referencedAssets'][K] extends 'image' ? (ReferencedAssetSource & { type?: 'image' }) | RiveImage - : ReferencedAssetSource & { type?: S['assets'][K] & RiveAssetType }; + : ReferencedAssetSource & { + type?: S['referencedAssets'][K] & RiveAssetType; + }; } >; @@ -71,10 +73,10 @@ export type TypedResolvedReferencedAssets = S, ResolvedReferencedAssets, { - [K in SchemaAssetKeys]?: S['assets'][K] extends 'image' + [K in SchemaAssetKeys]?: S['referencedAssets'][K] extends 'image' ? ResolvedReferencedAsset & { type?: 'image' } : Omit & { - type?: S['assets'][K] & RiveAssetType; + type?: S['referencedAssets'][K] & RiveAssetType; }; } >; diff --git a/src/core/TypedRiveFile.ts b/src/core/TypedRiveFile.ts index f69eab1a..dd1214c8 100644 --- a/src/core/TypedRiveFile.ts +++ b/src/core/TypedRiveFile.ts @@ -16,11 +16,11 @@ export interface RiveFileSchema { enums: 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'. + * 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'. */ - assets: Record; + referencedAssets: Record; } /** From 72e18c28d96d9d6fc1b366b8aa1a6e5797364c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 23 Sep 2026 11:43:13 +0200 Subject: [PATCH 08/11] refactor(schema): name image view model properties assetImage The web runtime reports image properties as 'image', which the schema passed through; that collides with the 'image' referenced-asset kind in the same file. 'assetImage' matches RML's ViewModelPropertyAssetImage and the native ViewModelPropertyType. --- example/assets/rive/databinding_images.riv.d.ts | 2 +- example/assets/rive/many_viewmodels.riv.d.ts | 2 +- scripts/__tests__/rive-gen-emit.test.ts | 9 +++++++++ scripts/rive-extract-schema.ts | 3 ++- scripts/rive-gen-types.ts | 12 +++++++++++- src/__tests__/typed-rive.test-d.ts | 9 +++++++++ src/core/TypedViewModelInstance.ts | 2 +- 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/example/assets/rive/databinding_images.riv.d.ts b/example/assets/rive/databinding_images.riv.d.ts index e580a0d4..b6d979b0 100644 --- a/example/assets/rive/databinding_images.riv.d.ts +++ b/example/assets/rive/databinding_images.riv.d.ts @@ -11,7 +11,7 @@ declare const asset: RiveAsset<{ enums: {}; viewModels: { MyViewModel: { - bound_image: 'image'; + bound_image: 'assetImage'; }; }; referencedAssets: {}; diff --git a/example/assets/rive/many_viewmodels.riv.d.ts b/example/assets/rive/many_viewmodels.riv.d.ts index 4673f98c..e9412d10 100644 --- a/example/assets/rive/many_viewmodels.riv.d.ts +++ b/example/assets/rive/many_viewmodels.riv.d.ts @@ -11,7 +11,7 @@ declare const asset: RiveAsset<{ enums: {}; viewModels: { First: { - imageValue: 'image'; + imageValue: 'assetImage'; colorValue: 'color'; textValue: 'string'; }; diff --git a/scripts/__tests__/rive-gen-emit.test.ts b/scripts/__tests__/rive-gen-emit.test.ts index bc861e20..7c8fddb3 100644 --- a/scripts/__tests__/rive-gen-emit.test.ts +++ b/scripts/__tests__/rive-gen-emit.test.ts @@ -10,6 +10,7 @@ import { assetsRecord, classifyAsset, enumPropTypeString, + propertyTypeString, collectEnums, viewModelRefTypeString, type Schema, @@ -256,3 +257,11 @@ describe('assetsRecord', () => { 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/rive-extract-schema.ts b/scripts/rive-extract-schema.ts index 9e064acc..b716d549 100644 --- a/scripts/rive-extract-schema.ts +++ b/scripts/rive-extract-schema.ts @@ -18,6 +18,7 @@ import { createAssetCollector, enumPropTypeString, nameMap, + propertyTypeString, viewModelRefTypeString, type RuntimeProperty, } from './rive-gen-types.ts'; @@ -120,7 +121,7 @@ async function main() { } 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; diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index 5b406321..1670af1e 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -187,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; @@ -414,6 +414,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 ac4d4889..443db2c6 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -34,6 +34,7 @@ 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 @@ -616,3 +617,11 @@ declare const handWrittenAsset: RiveAsset; 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')); 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( From f84b5b7af859abbcf4ae487e92f1a2d98fa2aad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 23 Sep 2026 15:40:47 +0200 Subject: [PATCH 09/11] fix(scripts): collect referenced assets into a null-prototype map --- scripts/rive-gen-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index 1670af1e..3ccc28f6 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -256,7 +256,7 @@ export function createAssetCollector(runtime: any): { assetLoader: any; assets: Record; } { - const assets: Record = {}; + const assets = nameMap(); const assetLoader = new runtime.CustomFileAssetLoader({ loadContents: (asset: any, embeddedBytes: Uint8Array | undefined) => { const classified = classifyAsset(asset ?? {}, embeddedBytes?.length ?? 0); From 0653cd78bb456c01fbc5d5eb716a6245ed9ff4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 24 Sep 2026 10:15:27 +0200 Subject: [PATCH 10/11] feat(schema)!: version generated schemas and fail loudly when outdated A .riv.d.ts from an older generator used to type-check silently as untyped: skipLibCheck hides the schema's own constraint error inside the .d.ts, so after an upgrade the typing just disappeared. Generated schemas now carry schemaVersion, and a RiveAsset whose schema no longer satisfies RiveFileSchema (a missing key or a different version) resolves to an error type named after the fix, so every use site reports: This .riv.d.ts is out of date: regenerate it with npx rive-gen-types. schemaVersion is bumped on every breaking format change; it also catches changes the shape alone cannot, like a renamed property type string. --- example/assets/rive/GradientBorder.riv.d.ts | 1 + .../rive/arbtboards-models-instances.riv.d.ts | 1 + example/assets/rive/artboard_db_test.riv.d.ts | 1 + example/assets/rive/blinko.riv.d.ts | 1 + example/assets/rive/bouncing_ball.riv.d.ts | 1 + example/assets/rive/click-count.riv.d.ts | 1 + example/assets/rive/counter.riv.d.ts | 1 + example/assets/rive/databinding.riv.d.ts | 1 + .../assets/rive/databinding_images.riv.d.ts | 1 + .../assets/rive/databinding_lists.riv.d.ts | 1 + example/assets/rive/fallback_fonts.riv.d.ts | 1 + example/assets/rive/finished_trigger.riv.d.ts | 1 + example/assets/rive/font_fallback.riv.d.ts | 1 + example/assets/rive/hello_world_text.riv.d.ts | 1 + example/assets/rive/inputglow.riv.d.ts | 1 + .../ios_android_layouts_demo_v01.riv.d.ts | 1 + example/assets/rive/layout_test.riv.d.ts | 1 + example/assets/rive/layouts_demo.riv.d.ts | 1 + example/assets/rive/many_viewmodels.riv.d.ts | 1 + example/assets/rive/movecircle.riv.d.ts | 1 + .../assets/rive/nodefaultbouncing.riv.d.ts | 1 + example/assets/rive/on_entry_test.riv.d.ts | 1 + example/assets/rive/out_of_band.riv.d.ts | 1 + example/assets/rive/quick_start.riv.d.ts | 1 + example/assets/rive/rating.riv.d.ts | 1 + example/assets/rive/rewards.riv.d.ts | 1 + .../assets/rive/style_fallback_fonts.riv.d.ts | 1 + example/assets/rive/tabtest.riv.d.ts | 1 + .../assets/rive/viewmodelproperty.riv.d.ts | 1 + .../assets/rive/vm_value_change_test.riv.d.ts | 1 + expo-example/assets/rive/rating.riv.d.ts | 1 + .../assets/rive/rewards_source.riv.d.ts | 1 + scripts/rive-gen-types.ts | 8 ++++++-- src/__tests__/typed-rive.test-d.ts | 17 ++++++++++++++++ src/core/RiveFile.ts | 8 ++++++-- src/core/TypedRiveFile.ts | 20 ++++++++++++++++--- src/hooks/useRiveFile.ts | 4 ++-- 37 files changed, 80 insertions(+), 9 deletions(-) diff --git a/example/assets/rive/GradientBorder.riv.d.ts b/example/assets/rive/GradientBorder.riv.d.ts index ac3a0306..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: { diff --git a/example/assets/rive/arbtboards-models-instances.riv.d.ts b/example/assets/rive/arbtboards-models-instances.riv.d.ts index 42ffd90d..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: { diff --git a/example/assets/rive/artboard_db_test.riv.d.ts b/example/assets/rive/artboard_db_test.riv.d.ts index ed172f33..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: { diff --git a/example/assets/rive/blinko.riv.d.ts b/example/assets/rive/blinko.riv.d.ts index 9a51eddd..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: { diff --git a/example/assets/rive/bouncing_ball.riv.d.ts b/example/assets/rive/bouncing_ball.riv.d.ts index 3cfed0d9..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: { diff --git a/example/assets/rive/click-count.riv.d.ts b/example/assets/rive/click-count.riv.d.ts index 82e2d7ce..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: { diff --git a/example/assets/rive/counter.riv.d.ts b/example/assets/rive/counter.riv.d.ts index 0b5660bd..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: { diff --git a/example/assets/rive/databinding.riv.d.ts b/example/assets/rive/databinding.riv.d.ts index f14771dd..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: { diff --git a/example/assets/rive/databinding_images.riv.d.ts b/example/assets/rive/databinding_images.riv.d.ts index b6d979b0..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: { diff --git a/example/assets/rive/databinding_lists.riv.d.ts b/example/assets/rive/databinding_lists.riv.d.ts index 77711784..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: { diff --git a/example/assets/rive/fallback_fonts.riv.d.ts b/example/assets/rive/fallback_fonts.riv.d.ts index 20a1d948..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: { diff --git a/example/assets/rive/finished_trigger.riv.d.ts b/example/assets/rive/finished_trigger.riv.d.ts index 838a398c..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: { diff --git a/example/assets/rive/font_fallback.riv.d.ts b/example/assets/rive/font_fallback.riv.d.ts index 11c9f814..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: { diff --git a/example/assets/rive/hello_world_text.riv.d.ts b/example/assets/rive/hello_world_text.riv.d.ts index 56b24a8e..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: { diff --git a/example/assets/rive/inputglow.riv.d.ts b/example/assets/rive/inputglow.riv.d.ts index f7456ff3..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: { 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 19f1e125..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: { diff --git a/example/assets/rive/layout_test.riv.d.ts b/example/assets/rive/layout_test.riv.d.ts index 80b1075b..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: { diff --git a/example/assets/rive/layouts_demo.riv.d.ts b/example/assets/rive/layouts_demo.riv.d.ts index 65285d57..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: { diff --git a/example/assets/rive/many_viewmodels.riv.d.ts b/example/assets/rive/many_viewmodels.riv.d.ts index e9412d10..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: { diff --git a/example/assets/rive/movecircle.riv.d.ts b/example/assets/rive/movecircle.riv.d.ts index aee98b60..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: { diff --git a/example/assets/rive/nodefaultbouncing.riv.d.ts b/example/assets/rive/nodefaultbouncing.riv.d.ts index 6405947d..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: { diff --git a/example/assets/rive/on_entry_test.riv.d.ts b/example/assets/rive/on_entry_test.riv.d.ts index 5f97bd05..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: { diff --git a/example/assets/rive/out_of_band.riv.d.ts b/example/assets/rive/out_of_band.riv.d.ts index 54504bea..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: { diff --git a/example/assets/rive/quick_start.riv.d.ts b/example/assets/rive/quick_start.riv.d.ts index f7103a8c..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: { diff --git a/example/assets/rive/rating.riv.d.ts b/example/assets/rive/rating.riv.d.ts index 923c4d1a..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: { diff --git a/example/assets/rive/rewards.riv.d.ts b/example/assets/rive/rewards.riv.d.ts index 5d446579..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: { diff --git a/example/assets/rive/style_fallback_fonts.riv.d.ts b/example/assets/rive/style_fallback_fonts.riv.d.ts index 92e1821e..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: { diff --git a/example/assets/rive/tabtest.riv.d.ts b/example/assets/rive/tabtest.riv.d.ts index 23a3b845..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: { diff --git a/example/assets/rive/viewmodelproperty.riv.d.ts b/example/assets/rive/viewmodelproperty.riv.d.ts index c967d944..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: { 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 58bfad25..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: { diff --git a/expo-example/assets/rive/rating.riv.d.ts b/expo-example/assets/rive/rating.riv.d.ts index 923c4d1a..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: { diff --git a/expo-example/assets/rive/rewards_source.riv.d.ts b/expo-example/assets/rive/rewards_source.riv.d.ts index a1a0b91d..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: { diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index 3ccc28f6..5830b1ed 100644 --- a/scripts/rive-gen-types.ts +++ b/scripts/rive-gen-types.ts @@ -275,13 +275,16 @@ export function assetsRecord(assets: Record): string { .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/referencedAssets — omitting either 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 };` @@ -291,6 +294,7 @@ export function schemaBody(schema: Schema): string { ? `\n referencedAssets: {\n${assetsRecord(schema.referencedAssets)}\n };` : '\n referencedAssets: {};'; return `\ + schemaVersion: ${SCHEMA_VERSION}; artboards: ${schema.artboards.map(strLit).join(' | ')}; defaultArtboard: ${strLit(schema.defaultArtboard)}; stateMachines: { diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts index 443db2c6..51e7c3a5 100644 --- a/src/__tests__/typed-rive.test-d.ts +++ b/src/__tests__/typed-rive.test-d.ts @@ -267,6 +267,7 @@ 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' }; @@ -279,6 +280,7 @@ 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' }; @@ -290,6 +292,7 @@ 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' }; @@ -603,6 +606,7 @@ 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; @@ -625,3 +629,16 @@ type ImagesSchema = 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/RiveFile.ts b/src/core/RiveFile.ts index 51259df8..a75f9c72 100644 --- a/src/core/RiveFile.ts +++ b/src/core/RiveFile.ts @@ -1,6 +1,10 @@ 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 { @@ -161,7 +165,7 @@ export namespace RiveFileFactory { // 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: RiveAsset | { uri: string }, + source: SchemaBranded | { uri: string }, typedReferencedAssets: TypedResolvedReferencedAssets | undefined, loadCdn?: boolean ): Promise> { diff --git a/src/core/TypedRiveFile.ts b/src/core/TypedRiveFile.ts index dd1214c8..9464b8a0 100644 --- a/src/core/TypedRiveFile.ts +++ b/src/core/TypedRiveFile.ts @@ -9,6 +9,12 @@ 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; @@ -32,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/hooks/useRiveFile.ts b/src/hooks/useRiveFile.ts index b993f158..ad4df84a 100644 --- a/src/hooks/useRiveFile.ts +++ b/src/hooks/useRiveFile.ts @@ -14,9 +14,9 @@ import type { TypedReferencedAssets, } from '../core/ReferencedAssets'; import type { - RiveAsset, RiveFileSchema, TypedRiveFile, + SchemaBranded, } from '../core/TypedRiveFile'; export type { ReferencedAssets, ResolvedReferencedAssets }; @@ -114,7 +114,7 @@ type UseRiveFileResultFor = RiveFileSchema extends T // 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, + input: SchemaBranded | Exclude | undefined, options?: { referencedAssets?: TypedReferencedAssets } ): UseRiveFileResultFor; export function useRiveFile( From 930435f67eae8a86cce11041413e46a0e1e36b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 24 Sep 2026 10:21:41 +0200 Subject: [PATCH 11/11] refactor(scripts): make rive-extract-schema a thin wrapper around extractSchema The schema-building loop existed in both scripts, so every schema change had to be made twice. rive-extract-schema now prints rive-gen-types' extractSchema() as JSON; output is byte-identical for all 32 assets. --- scripts/rive-extract-schema.ts | 153 ++++----------------------------- scripts/rive-gen-types.ts | 4 +- 2 files changed, 19 insertions(+), 138 deletions(-) diff --git a/scripts/rive-extract-schema.ts b/scripts/rive-extract-schema.ts index b716d549..10214557 100644 --- a/scripts/rive-extract-schema.ts +++ b/scripts/rive-extract-schema.ts @@ -1,150 +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, - createAssetCollector, - enumPropTypeString, - nameMap, - propertyTypeString, - 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, assets } = createAssetCollector(runtime); - - 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] = propertyTypeString(p.type); - } - } - viewModels[vm.name] = props; - } - - const defaultArtboard = artboards[0] ?? ''; - process.stdout.write( - JSON.stringify( - { - artboards, - defaultArtboard, - stateMachines, - enums, - viewModels, - referencedAssets: assets, - }, - null, - 2 - ) + '\n' - ); -} -main().catch((err: Error) => { - process.stderr.write(err.message + '\n'); - process.exit(1); -}); +// 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); + }); diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts index 5830b1ed..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 }), @@ -126,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);