diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 066f4f5e..eb63fb47 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -9,6 +9,9 @@ runs: with: node-version-file: .nvmrc + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + - name: Restore dependencies id: yarn-cache uses: actions/cache/restore@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b370329..9a7f1d8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,14 @@ jobs: exit 1 fi + - name: Validate .riv schema files + run: | + yarn rive-gen-types --all example/assets/rive + if ! git diff --exit-code 'example/assets/rive/*.riv.d.ts'; then + echo "Error: .riv.d.ts files are out of date. Please run 'yarn rive-gen-types --all example/assets/rive' and commit the changes." + exit 1 + fi + - name: Typecheck files run: yarn typecheck diff --git a/eslint.config.mjs b/eslint.config.mjs index 447ee604..55355c63 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -54,6 +54,8 @@ export default defineConfig([ 'node_modules/', 'lib/', '**/.expo/', + '**/*.test-d.ts', + '**/*.riv.d.ts', ], }, ]); diff --git a/example/assets/rive/GradientBorder.riv b/example/assets/rive/GradientBorder.riv new file mode 100644 index 00000000..aa883225 Binary files /dev/null and b/example/assets/rive/GradientBorder.riv differ diff --git a/example/assets/rive/GradientBorder.riv.d.ts b/example/assets/rive/GradientBorder.riv.d.ts new file mode 100644 index 00000000..a964f379 --- /dev/null +++ b/example/assets/rive/GradientBorder.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: GradientBorder.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Layout'; + defaultArtboard: 'Layout'; + stateMachines: { + Layout: 'State'; + }; + viewModels: { + ViewModel: { + isFocused: 'boolean'; + }; + }; +}>; +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 new file mode 100644 index 00000000..6b8b0eb6 --- /dev/null +++ b/example/assets/rive/artboard_db_test.riv.d.ts @@ -0,0 +1,21 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: artboard_db_test.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Main' | 'ArtboardGreen' | 'ArtboardBlue' | 'ArtboardRed'; + defaultArtboard: 'Main'; + stateMachines: { + Main: 'State Machine 1'; + ArtboardGreen: 'State Machine 1'; + ArtboardBlue: 'State Machine 1'; + ArtboardRed: 'State Machine 1'; + }; + viewModels: { + MainViewModel: { + artboard_1: 'artboard'; + artboard_2: 'artboard'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/blinko.riv.d.ts b/example/assets/rive/blinko.riv.d.ts new file mode 100644 index 00000000..8266f094 --- /dev/null +++ b/example/assets/rive/blinko.riv.d.ts @@ -0,0 +1,97 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: blinko.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Main' | 'StoreItem' | 'Token' | 'Indicator' | 'Puck' | 'StoreIcon' | 'Peg' | 'scoreBox'; + defaultArtboard: 'Main'; + stateMachines: { + Main: 'State Machine 1'; + StoreItem: 'State Machine 1'; + Token: 'State Machine 1'; + Indicator: 'State Machine 1'; + Puck: 'State Machine 1'; + StoreIcon: 'State Machine 1'; + Peg: 'State Machine 1'; + scoreBox: 'State Machine 1'; + }; + viewModels: { + IndicatorVM: { + purchased: 'boolean'; + }; + StoreItemVM: { + tokenCount: 'number'; + height: 'number'; + itemHover: 'boolean'; + cost: 'number'; + description: 'string'; + id: 'string'; + itemType: 'string'; + maxPurchases: 'number'; + name: 'string'; + purchase: 'trigger'; + purchaseCount: 'number'; + }; + storeIconVM: { + storeiconClick: 'trigger'; + iconHovered: 'boolean'; + }; + storeVM: { + 'xbuttonClick': 'trigger'; + 'xbuttonHover': 'boolean'; + 'items': 'list'; + 'storeOpen': 'boolean'; + 'property of pegVM': 'viewModel:pegVM'; + 'multiplierValue': 'number'; + 'extraTokenSlotActive': 'boolean'; + 'extraTokenSlotCost': 'number'; + 'extraTokenSlotCount': 'number'; + 'purchaseExtraTokenSlot': 'trigger'; + 'purchaseSuperMultiplier': 'trigger'; + 'superMultiplierActive': 'boolean'; + 'superMultiplierCost': 'number'; + 'superMultiplierCount': 'number'; + }; + TokenVM: { + spinToken: 'boolean'; + }; + pegVM: { + blink: 'trigger'; + multiplierValue: 'number'; + pegType: 'enum:normal|multiplier'; + pegBounced: 'trigger'; + }; + gameLogicVM: { + turnTokens: 'number'; + turnScore: 'number'; + buttonLabel: 'string'; + message: 'string'; + startTurn: 'trigger'; + turnEnded: 'trigger'; + gameEnded: 'boolean'; + tokenCount: 'number'; + scoreTotal: 'number'; + }; + scoreBox: { + Token: 'viewModel:TokenVM'; + slotType: 'enum:addToken|normal'; + landed: 'trigger'; + scoreValue: 'number'; + }; + PuckVM: { + puckBounce: 'trigger'; + puckRotation: 'number'; + }; + main: { + 'PuckVM': 'viewModel:PuckVM'; + 'storeItemVM': 'viewModel:StoreItemVM'; + 'storeIconVM': 'viewModel:storeIconVM'; + 'pegVM': 'viewModel:pegVM'; + 'storeVM': 'viewModel:storeVM'; + 'buttonHover': 'boolean'; + 'tokenVM': 'viewModel:TokenVM'; + 'property of gameLogicVM': 'viewModel:gameLogicVM'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/bouncing_ball.riv.d.ts b/example/assets/rive/bouncing_ball.riv.d.ts new file mode 100644 index 00000000..10bc5f04 --- /dev/null +++ b/example/assets/rive/bouncing_ball.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: bouncing_ball.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + vm: { + ypos: 'number'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/counter.riv.d.ts b/example/assets/rive/counter.riv.d.ts new file mode 100644 index 00000000..244bbe14 --- /dev/null +++ b/example/assets/rive/counter.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: counter.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + ViewModel1: { + cnt: 'number'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/databinding.riv.d.ts b/example/assets/rive/databinding.riv.d.ts new file mode 100644 index 00000000..f0b630e5 --- /dev/null +++ b/example/assets/rive/databinding.riv.d.ts @@ -0,0 +1,28 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: databinding.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + Person: { + pet: 'viewModel:Pet'; + jump: 'trigger'; + likes_popcorn: 'boolean'; + favourite_pet: 'enum:chipmunk|rat|frog|owl|cat|dog'; + favourite_color: 'color'; + age: 'number'; + website: 'string'; + name: 'string'; + }; + Pet: { + type: 'enum:chipmunk|rat|frog|owl|cat|dog'; + name: 'string'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/databinding_images.riv.d.ts b/example/assets/rive/databinding_images.riv.d.ts new file mode 100644 index 00000000..b9d93374 --- /dev/null +++ b/example/assets/rive/databinding_images.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: databinding_images.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + MyViewModel: { + bound_image: 'image'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/databinding_lists.riv.d.ts b/example/assets/rive/databinding_lists.riv.d.ts new file mode 100644 index 00000000..00334476 --- /dev/null +++ b/example/assets/rive/databinding_lists.riv.d.ts @@ -0,0 +1,20 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: databinding_lists.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + Person: { + name: 'string'; + }; + DevRel: { + team: 'list'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/fallback_fonts.riv.d.ts b/example/assets/rive/fallback_fonts.riv.d.ts new file mode 100644 index 00000000..60e8aa1d --- /dev/null +++ b/example/assets/rive/fallback_fonts.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: fallback_fonts.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/font_fallback.riv.d.ts b/example/assets/rive/font_fallback.riv.d.ts new file mode 100644 index 00000000..02bdda87 --- /dev/null +++ b/example/assets/rive/font_fallback.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: font_fallback.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + ViewModel: { + text: 'string'; + }; + }; +}>; +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 new file mode 100644 index 00000000..4e879f1e --- /dev/null +++ b/example/assets/rive/hello_world_text.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: hello_world_text.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'New Artboard'; + defaultArtboard: 'New Artboard'; + stateMachines: { + 'New Artboard': 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/inputglow.riv b/example/assets/rive/inputglow.riv new file mode 100644 index 00000000..85e96d39 Binary files /dev/null and b/example/assets/rive/inputglow.riv differ diff --git a/example/assets/rive/inputglow.riv.d.ts b/example/assets/rive/inputglow.riv.d.ts new file mode 100644 index 00000000..c1a84c21 --- /dev/null +++ b/example/assets/rive/inputglow.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: inputglow.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Layout'; + defaultArtboard: 'Layout'; + stateMachines: { + Layout: 'State'; + }; + viewModels: { + ViewModel: { + isFocused: 'boolean'; + }; + }; +}>; +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 new file mode 100644 index 00000000..549ba94c --- /dev/null +++ b/example/assets/rive/ios_android_layouts_demo_v01.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: ios_android_layouts_demo_v01.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'iOS_Android_Layouts_demo_v01'; + defaultArtboard: 'iOS_Android_Layouts_demo_v01'; + stateMachines: { + iOS_Android_Layouts_demo_v01: 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/layout_test.riv b/example/assets/rive/layout_test.riv new file mode 100644 index 00000000..24f2f818 Binary files /dev/null and b/example/assets/rive/layout_test.riv differ diff --git a/example/assets/rive/layout_test.riv.d.ts b/example/assets/rive/layout_test.riv.d.ts new file mode 100644 index 00000000..25234ee9 --- /dev/null +++ b/example/assets/rive/layout_test.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: layout_test.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + ViewModel1: { + + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/layouts_demo.riv.d.ts b/example/assets/rive/layouts_demo.riv.d.ts new file mode 100644 index 00000000..ac330ad0 --- /dev/null +++ b/example/assets/rive/layouts_demo.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: layouts_demo.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'iOS_Android_Layouts_demo_v01'; + defaultArtboard: 'iOS_Android_Layouts_demo_v01'; + stateMachines: { + iOS_Android_Layouts_demo_v01: 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/many_viewmodels.riv.d.ts b/example/assets/rive/many_viewmodels.riv.d.ts new file mode 100644 index 00000000..4e717058 --- /dev/null +++ b/example/assets/rive/many_viewmodels.riv.d.ts @@ -0,0 +1,19 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: many_viewmodels.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + First: { + imageValue: 'image'; + colorValue: 'color'; + textValue: 'string'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/movecircle.riv.d.ts b/example/assets/rive/movecircle.riv.d.ts new file mode 100644 index 00000000..229ae35c --- /dev/null +++ b/example/assets/rive/movecircle.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: movecircle.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + ViewModel: { + posY: 'number'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/nodefaultbouncing.riv.d.ts b/example/assets/rive/nodefaultbouncing.riv.d.ts new file mode 100644 index 00000000..60e32056 --- /dev/null +++ b/example/assets/rive/nodefaultbouncing.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: nodefaultbouncing.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + VM1: { + + }; + }; +}>; +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 new file mode 100644 index 00000000..56b087dc --- /dev/null +++ b/example/assets/rive/on_entry_test.riv.d.ts @@ -0,0 +1,18 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: on_entry_test.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + vm: { + input: 'number'; + inputOnEntry: 'number'; + }; + }; +}>; +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 new file mode 100644 index 00000000..9546dc6a --- /dev/null +++ b/example/assets/rive/out_of_band.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: out_of_band.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/quick_start.riv.d.ts b/example/assets/rive/quick_start.riv.d.ts new file mode 100644 index 00000000..4847c6a7 --- /dev/null +++ b/example/assets/rive/quick_start.riv.d.ts @@ -0,0 +1,21 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: quick_start.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'health_bar_v01'; + defaultArtboard: 'health_bar_v01'; + stateMachines: { + health_bar_v01: 'State Machine 1'; + }; + viewModels: { + health_bar_01: { + gameOver: 'trigger'; + hoverYes: 'boolean'; + hoverNo: 'boolean'; + healthColor: 'color'; + health: 'number'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/rating.riv.d.ts b/example/assets/rive/rating.riv.d.ts new file mode 100644 index 00000000..35be7041 --- /dev/null +++ b/example/assets/rive/rating.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: rating.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'New Artboard'; + defaultArtboard: 'New Artboard'; + stateMachines: { + 'New Artboard': 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/rewards.riv.d.ts b/example/assets/rive/rewards.riv.d.ts new file mode 100644 index 00000000..85ed68a4 --- /dev/null +++ b/example/assets/rive/rewards.riv.d.ts @@ -0,0 +1,51 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: rewards.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Main' | 'Lives 2' | 'Item Value' | 'Chest' | 'Button' | 'Heart' | 'Item'; + defaultArtboard: 'Main'; + stateMachines: { + 'Main': 'State Machine 1'; + 'Lives 2': 'State Machine 1'; + 'Item Value': 'State Machine 1'; + 'Chest': 'State Machine 1'; + 'Button': 'State Machine 1'; + 'Heart': 'State Machine 1'; + 'Item': 'State Machine 1'; + }; + viewModels: { + Item_Icon_Value: { + Icon_React: 'trigger'; + Property_Of_Item: 'viewModel:Item'; + Item_Value: 'number'; + }; + Energy_Bar: { + Bar_Color: 'color'; + Lives: 'number'; + Energy_Bar: 'number'; + }; + Rewards: { + Price_Value: 'number'; + Color: 'color'; + Height: 'number'; + With: 'number'; + Item_Selection: 'viewModel:Item'; + Item_Value_Icon: 'viewModel:Item_Icon_Value'; + Button: 'viewModel:Button'; + Coin: 'viewModel:Item_Icon_Value'; + Gem: 'viewModel:Item_Icon_Value'; + Energy_Bar: 'viewModel:Energy_Bar'; + }; + Button: { + State_1: 'string'; + Item_Text: 'string'; + Item: 'viewModel:Item'; + Pressed: 'trigger'; + }; + Item: { + Item_Selection: 'enum:Coin|Gem'; + }; + }; +}>; +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 new file mode 100644 index 00000000..ecd23149 --- /dev/null +++ b/example/assets/rive/style_fallback_fonts.riv.d.ts @@ -0,0 +1,12 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: style_fallback_fonts.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; +}>; +export default asset; diff --git a/example/assets/rive/viewmodelproperty.riv.d.ts b/example/assets/rive/viewmodelproperty.riv.d.ts new file mode 100644 index 00000000..949846e8 --- /dev/null +++ b/example/assets/rive/viewmodelproperty.riv.d.ts @@ -0,0 +1,21 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: viewmodelproperty.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + root: { + vm2: 'viewModel:vm'; + vm1: 'viewModel:vm'; + }; + vm: { + name: 'string'; + }; + }; +}>; +export default asset; diff --git a/example/assets/rive/vm_value_change_test.riv b/example/assets/rive/vm_value_change_test.riv new file mode 100644 index 00000000..5dc931d6 Binary files /dev/null and b/example/assets/rive/vm_value_change_test.riv differ diff --git a/example/assets/rive/vm_value_change_test.riv.d.ts b/example/assets/rive/vm_value_change_test.riv.d.ts new file mode 100644 index 00000000..d092fc1f --- /dev/null +++ b/example/assets/rive/vm_value_change_test.riv.d.ts @@ -0,0 +1,18 @@ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: vm_value_change_test.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + TextModel: { + trigger: 'trigger'; + textValue: 'string'; + }; + }; +}>; +export default asset; diff --git a/example/src/exercisers/FileSwitcher.tsx b/example/src/exercisers/FileSwitcher.tsx new file mode 100644 index 00000000..d29671b6 --- /dev/null +++ b/example/src/exercisers/FileSwitcher.tsx @@ -0,0 +1,207 @@ +/** + * File Switcher Exerciser + * + * Rapidly switch between .riv files with different fit modes to test for + * crashes related to dispose/render races. + * + * Reproduces "Should not already be working" crash seen on RN 0.83. + */ + +import { useState } from 'react'; +import { + type DimensionValue, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { Fit, RiveView, useRiveFile } from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +const FILES = [ + { + label: 'layout_test', + source: require('../../assets/rive/layout_test.riv'), + }, + { label: 'inputglow', source: require('../../assets/rive/inputglow.riv') }, + { + label: 'GradientBorder', + source: require('../../assets/rive/GradientBorder.riv'), + }, + { + label: 'layouts_demo', + source: require('../../assets/rive/layouts_demo.riv'), + }, + { + label: 'bouncing_ball', + source: require('../../assets/rive/bouncing_ball.riv'), + }, + { label: 'counter', source: require('../../assets/rive/counter.riv') }, + { label: 'rating', source: require('../../assets/rive/rating.riv') }, +] as const; + +const FITS = [ + { label: 'Layout', value: Fit.Layout }, + { label: 'Contain', value: Fit.Contain }, + { label: 'Cover', value: Fit.Cover }, + { label: 'Fill', value: Fit.Fill }, +] as const; + +const SIZES = [ + { label: 'full x 150', width: '100%' as const, height: 150 }, + { label: 'full x 80', width: '100%' as const, height: 80 }, + { label: '200 x 120', width: 200, height: 120 }, +]; + +function RiveBox({ + source, + fit, + height, + width, +}: { + source: number; + fit: Fit; + height: number; + width: DimensionValue; +}) { + const { riveFile } = useRiveFile(source); + + return ( + + {riveFile && ( + + )} + + ); +} + +function Chip({ + label, + selected, + onPress, +}: { + label: string; + selected: boolean; + onPress: () => void; +}) { + return ( + + + {label} + + + ); +} + +export const metadata: Metadata = { + name: 'File Switcher', + description: 'Rapidly switch files/fits to test dispose/render race crashes', +}; + +export default function FileSwitcher() { + const [fileIdx, setFileIdx] = useState(0); + const [fitIdx, setFitIdx] = useState(0); + + const file = FILES[fileIdx]!; + const fit = FITS[fitIdx]!; + + return ( + + File + + {FILES.map((f, i) => ( + setFileIdx(i)} + /> + ))} + + + Fit + + {FITS.map((f, i) => ( + setFitIdx(i)} + /> + ))} + + + + {file.label} — Fit.{fit.label} + + {SIZES.map((size) => ( + + {size.label} + + + ))} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0c1027', + }, + content: { + padding: 20, + paddingBottom: 60, + }, + section: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + marginTop: 16, + marginBottom: 8, + }, + chips: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + chip: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + borderWidth: 1, + borderColor: '#555', + }, + chipSelected: { + backgroundColor: '#4f6ef7', + borderColor: '#4f6ef7', + }, + chipText: { + color: '#999', + fontSize: 13, + }, + chipTextSelected: { + color: '#fff', + }, + item: { + marginBottom: 16, + }, + label: { + color: '#999', + fontSize: 12, + marginBottom: 4, + }, + riveBox: { + borderWidth: 1, + borderColor: 'red', + }, +}); diff --git a/example/src/exercisers/RiveDataBindingExample.tsx b/example/src/exercisers/RiveDataBindingExample.tsx index 825c81f7..3b6ebc06 100644 --- a/example/src/exercisers/RiveDataBindingExample.tsx +++ b/example/src/exercisers/RiveDataBindingExample.tsx @@ -5,19 +5,18 @@ import { RiveView, useRiveNumber, useViewModelInstance, - type ViewModelInstance, - type RiveFile, + type TypedViewModelOf, + type TypedRiveFile, useRiveString, useRiveColor, useRiveTrigger, useRiveFile, } from '@rive-app/react-native'; import { type Metadata } from '../shared/metadata'; +import rewardsRiv from '../../assets/rive/rewards.riv'; export default function WithRiveFile() { - const { riveFile, isLoading, error } = useRiveFile( - require('../../assets/rive/rewards.riv') - ); + const { riveFile, isLoading, error } = useRiveFile(rewardsRiv); return ( @@ -36,12 +35,18 @@ export default function WithRiveFile() { ); } -function WithViewModelSetup({ file }: { file: RiveFile }) { - const { instance, error } = useViewModelInstance(file); +function WithViewModelSetup({ + file, +}: { + file: TypedRiveFile; +}) { + const { instance, error } = useViewModelInstance(file, { + viewModelName: 'Rewards', + }); if (error) { console.error(error.message); - return {error.message}; + return {error.message}; } if (!instance) { @@ -55,8 +60,8 @@ function DataBindingExample({ instance, file, }: { - instance: ViewModelInstance; - file: RiveFile; + instance: TypedViewModelOf; + file: TypedRiveFile; }) { const { error: coinValueError } = useRiveNumber('Coin/Item_Value', instance); @@ -64,7 +69,10 @@ function DataBindingExample({ console.error('coinValueError', coinValueError); } - const { setValue: setButtonText } = useRiveString('Button/State_1', instance); + const { setValue: setButtonText } = useRiveString( + 'Button/Item_Text', + instance + ); const { setValue: setBarColor, error: barColorError } = useRiveColor( 'Energy_Bar/Bar_Color', diff --git a/package.json b/package.json index 7688d957..c0fa7400 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,16 @@ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "prepare": "npx react-native-builder-bob@^0.40.0 build", "nitrogen": "nitrogen && npx tsx scripts/nitrogen-postprocess.ts", + "rive-gen-types": "bun scripts/rive-gen-types.ts", "release": "release-it", "dev:ios": "cd example && xed ios", "dev:android": "cd example && open -a \"/Applications/Android Studio.app\" ./android", "copy:nitrogen-config": "mkdir -p lib/nitrogen/generated/shared/json && cp nitrogen/generated/shared/json/RiveViewConfig.json lib/nitrogen/generated/shared/json/", "lint:swift": "./scripts/lint-swift.sh", "lint:kotlin": "./scripts/lint-kotlin.sh", - "lint:native": "yarn lint:swift && yarn lint:kotlin" + "lint:native": "yarn lint:swift && yarn lint:kotlin", + "typetest": "tsd --typings src/index.tsx", + "test:scripts": "cd scripts/__tests__ && bun test --no-parallel" }, "keywords": [ "react-native", @@ -78,6 +81,7 @@ "@react-native/babel-preset": "0.79.2", "@react-native/eslint-config": "^0.78.0", "@release-it/conventional-changelog": "^9.0.2", + "@rive-app/canvas": "^2.38.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/react-native": "^13.3.3", "@types/jest": "^29.5.5", @@ -100,6 +104,7 @@ "react-native-nitro-modules": "0.35.0", "react-test-renderer": "19.0.0", "release-it": "^17.10.0", + "tsd": "^0.33.0", "turbo": "^1.10.7", "typescript": "^5.2.2" }, @@ -123,6 +128,10 @@ "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" ], + "testPathIgnorePatterns": [ + "/node_modules/", + "\\.test-d\\.ts$" + ], "modulePathIgnorePatterns": [ "/example/node_modules", "/lib/" @@ -188,6 +197,20 @@ ] ] }, + "tsd": { + "typings": "src/index.tsx", + "directory": "src/__tests__", + "compilerOptions": { + "paths": { + "@rive-app/react-native": ["./src/index"] + }, + "moduleResolution": "bundler", + "jsx": "react-jsx", + "lib": ["ESNext"], + "strict": true, + "allowArbitraryExtensions": true + } + }, "create-react-native-library": { "languages": "kotlin-swift", "type": "nitro-module", diff --git a/scripts/__tests__/rive-extract-schema.test.ts b/scripts/__tests__/rive-extract-schema.test.ts new file mode 100644 index 00000000..f3265977 --- /dev/null +++ b/scripts/__tests__/rive-extract-schema.test.ts @@ -0,0 +1,65 @@ +import { describe, test, expect, beforeAll } from 'bun:test'; +import { spawnSync } from 'child_process'; +import { resolve } from 'path'; + +const EXTRACTOR = resolve(__dirname, '../rive-extract-schema.ts'); +const REWARDS_RIV = resolve(__dirname, '../../example/assets/rive/rewards.riv'); + +function extract(path: string) { + const result = spawnSync('bun', [EXTRACTOR, path], { + encoding: 'utf8', + timeout: 30_000, + cwd: resolve(__dirname, '../..'), + }); + if (result.status !== 0) throw new Error(result.stderr ?? 'extractor failed'); + return JSON.parse(result.stdout) as Record; +} + +describe('rive-extract-schema', () => { + let schema: ReturnType; + + beforeAll(() => { + schema = extract(REWARDS_RIV); + }); + + test('extracts artboards', () => { + expect(schema.artboards).toContain('Main'); + expect(schema.defaultArtboard).toBe('Main'); + }); + + test('extracts state machines per artboard', () => { + expect((schema.stateMachines as Record).Main).toContain( + 'State Machine 1' + ); + }); + + test('extracts viewModels', () => { + expect( + (schema.viewModels as Record).Rewards + ).toBeDefined(); + }); + + test('resolves nested viewModel references', () => { + const vms = schema.viewModels as Record>; + expect(vms.Rewards!.Coin).toBe('viewModel:Item_Icon_Value'); + }); + + test('extracts primitive property types', () => { + const vms = schema.viewModels as Record>; + expect(vms.Item_Icon_Value!.Item_Value).toBe('number'); + expect(vms.Energy_Bar!.Bar_Color).toBe('color'); + }); + + test('extracts enum values as pipe-separated string', () => { + const databinding = extract( + resolve(__dirname, '../../example/assets/rive/databinding.riv') + ); + const vms = databinding.viewModels as Record< + string, + Record + >; + expect(vms.Person!.favourite_pet).toMatch(/^enum:/); + expect(vms.Person!.favourite_pet).toContain('chipmunk'); + expect(vms.Person!.favourite_pet).toContain('dog'); + }); +}); diff --git a/scripts/__tests__/rive-gen-types.test.ts b/scripts/__tests__/rive-gen-types.test.ts new file mode 100644 index 00000000..128b3e8d --- /dev/null +++ b/scripts/__tests__/rive-gen-types.test.ts @@ -0,0 +1,46 @@ +import { describe, test, expect, beforeAll } from 'bun:test'; +import { spawnSync } from 'child_process'; +import { resolve } from 'path'; +import { readFileSync, existsSync } from 'fs'; + +const GENERATOR = resolve(__dirname, '../rive-gen-types.ts'); +const REWARDS_RIV = resolve(__dirname, '../../example/assets/rive/rewards.riv'); +const OUT_DTS = `${REWARDS_RIV}.d.ts`; + +describe('rive-gen-types', () => { + beforeAll(() => { + const result = spawnSync('bun', [GENERATOR, REWARDS_RIV], { + encoding: 'utf8', + timeout: 30_000, + cwd: resolve(__dirname, '../..'), + }); + if (result.status !== 0) + throw new Error(result.stderr ?? 'generator failed'); + }); + + test('generates .riv.d.ts next to source file', () => { + expect(existsSync(OUT_DTS)).toBe(true); + }); + + test('generated file declares a RiveAsset default export', () => { + const content = readFileSync(OUT_DTS, 'utf8'); + expect(content).toContain('declare const asset: RiveAsset<'); + expect(content).toContain('export default asset'); + }); + + test('generated file contains artboard types', () => { + const content = readFileSync(OUT_DTS, 'utf8'); + expect(content).toContain("'Main'"); + }); + + test('generated file contains viewModel types', () => { + const content = readFileSync(OUT_DTS, 'utf8'); + expect(content).toContain("'Rewards'"); + expect(content).toContain("'viewModel:Item_Icon_Value'"); + }); + + test('generated file has eslint-disable header', () => { + const content = readFileSync(OUT_DTS, 'utf8'); + expect(content).toContain('eslint-disable'); + }); +}); diff --git a/scripts/rive-extract-schema.ts b/scripts/rive-extract-schema.ts new file mode 100644 index 00000000..6e14816f --- /dev/null +++ b/scripts/rive-extract-schema.ts @@ -0,0 +1,165 @@ +#!/usr/bin/env bun +/** + * Extracts artboard names and state machines from a .riv file and prints JSON to stdout. + * + * Usage: + * bun scripts/rive-extract-schema.ts path/to/file.riv + * bun 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'; +import { RuntimeLoader } from '@rive-app/canvas'; + +// 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.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(); + + // On headless Linux (no WebGL) the WASM image-load counter (aa.total/aa.loaded) + // never reaches its target because image texture creation silently fails, so + // load() never resolves. We patch makeRenderImage to wrap img.decode so that + // img.la() is always called after decode — a safe no-op if WebGL already fired + // it, but unblocks the Promise when WebGL is absent. + const origMRI = (runtime.renderFactory as any).makeRenderImage.bind( + runtime.renderFactory + ); + (runtime.renderFactory as any).makeRenderImage = function () { + const img = origMRI(); + if ( + img && + typeof (img as any).la === 'function' && + typeof (img as any).decode === 'function' + ) { + const origDecode = (img as any).decode.bind(img); + (img as any).decode = function (imgBytes: Uint8Array) { + origDecode(imgBytes); // fires la() internally if WebGL succeeds + // Defer la() to a microtask so it fires *after* the WASM's K() returns and + // assigns H. Calling la() synchronously inside K() would resolve the Promise + // with H=null (K hasn't returned yet), producing an empty file. + queueMicrotask(() => (img as any).la()); + }; + } + return img; + }; + + // CustomFileAssetLoader handles embedded assets so the runtime resolves load(). + // For fonts: decode() loads font bytes (no WebGL needed). + // For images: decode() triggers our patched img.decode → img.la() unblocks. + const assetLoader = new (runtime as any).CustomFileAssetLoader({ + loadContents: (asset: any, embeddedBytes: Uint8Array) => { + if (embeddedBytes?.length && asset?.decode) { + asset.decode(embeddedBytes); + } + return true; + }, + }); + + const riveFile = await runtime.load(bytes, assetLoader, false); + + const artboards: string[] = []; + const stateMachines: Record = {}; + 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 viewModels: Record> = {}; + 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 Array<{ + name: string; + type: string; + }>; + // Create a blank instance to resolve viewModel property references + const inst = vm.instance?.() as any; + const props: Record = {}; + for (const p of properties) { + if (p.type === 'viewModel' && inst) { + try { + const nested = inst.viewModel?.(p.name); + const refName = nested?.getViewModelName?.(); + props[p.name] = refName ? `viewModel:${refName}` : 'viewModel'; + } catch { + props[p.name] = 'viewModel'; + } + } else if (p.type === 'enumType' && inst) { + try { + const ep = inst.enum?.(p.name); + const values: string[] = ep?.values ?? []; + props[p.name] = + values.length > 0 ? `enum:${values.join('|')}` : 'enum'; + } catch { + props[p.name] = 'enum'; + } + } else { + props[p.name] = p.type; + } + } + viewModels[vm.name] = props; + } + + const defaultArtboard = artboards[0] ?? ''; + process.stdout.write( + JSON.stringify( + { artboards, defaultArtboard, stateMachines, viewModels }, + null, + 2 + ) + '\n' + ); +} + +main().catch((err: Error) => { + process.stderr.write(err.message + '\n'); + process.exit(1); +}); diff --git a/scripts/rive-gen-types.ts b/scripts/rive-gen-types.ts new file mode 100644 index 00000000..156ce065 --- /dev/null +++ b/scripts/rive-gen-types.ts @@ -0,0 +1,335 @@ +#!/usr/bin/env bun +/** + * Generates .riv.d.ts declaration files so that `import asset from './file.riv'` + * is automatically typed in TypeScript without any extra imports. + * + * Usage: + * bun scripts/rive-gen-types.ts # writes .riv.d.ts next to the source + * bun scripts/rive-gen-types.ts --out # write a standalone schema .ts instead + * bun scripts/rive-gen-types.ts --all # generate for every .riv in a directory + * + * After generation, TypeScript resolves the .riv.d.ts automatically: + * import gameRiv from './assets/game.riv'; // typed as RiveAsset + * const file = await RiveFileFactory.fromSource(gameRiv); // TypedRiveFile — T inferred + */ + +import { + readFileSync, + writeFileSync, + mkdirSync, + readdirSync, + statSync, +} from 'fs'; +import { dirname, resolve, basename, extname } from 'path'; +import { RuntimeLoader } from '@rive-app/canvas'; + +// Browser shims required by the @rive-app/canvas WASM runtime. +(globalThis as any).document = { + createElement: () => ({ getContext: () => null }), +}; +(globalThis as any).Image = class {}; + +// Silence WASM warnings (e.g. "No WebGL support") so they don't pollute output. +console.log = (...args: unknown[]) => + process.stderr.write(args.join(' ') + '\n'); +console.warn = (...args: unknown[]) => + process.stderr.write(args.join(' ') + '\n'); + +interface Schema { + artboards: string[]; + defaultArtboard: string; + stateMachines: Record; + viewModels: Record>; +} + +let runtimeReady: Promise | null = null; + +async function getRuntime(): Promise { + if (!runtimeReady) { + runtimeReady = RuntimeLoader.awaitInstance().then((runtime) => { + // On headless Linux (no WebGL) the image-load counter (aa.total/aa.loaded) + // never resolves. Wrap img.decode to fire img.la() via queueMicrotask after + // K() returns so the Promise resolves with the actual file, not null. + const origMRI = (runtime.renderFactory as any).makeRenderImage.bind( + runtime.renderFactory + ); + (runtime.renderFactory as any).makeRenderImage = function () { + const img = origMRI(); + if ( + img && + typeof (img as any).la === 'function' && + typeof (img as any).decode === 'function' + ) { + const origDecode = (img as any).decode.bind(img); + (img as any).decode = function (imgBytes: Uint8Array) { + origDecode(imgBytes); + queueMicrotask(() => (img as any).la()); + }; + } + return img; + }; + return runtime; + }); + } + return runtimeReady; +} + +async function extractSchema(input: string): Promise { + const bytes = + input.startsWith('http://') || input.startsWith('https://') + ? new Uint8Array(await (await fetch(input)).arrayBuffer()) + : new Uint8Array(readFileSync(input)); + + const runtime = await getRuntime(); + + const assetLoader = new (runtime as any).CustomFileAssetLoader({ + loadContents: (asset: any, embeddedBytes: Uint8Array) => { + if (embeddedBytes?.length && asset?.decode) { + asset.decode(embeddedBytes); + } + return true; + }, + }); + + const riveFile = await runtime.load(bytes, assetLoader, false); + + const artboards: string[] = []; + const stateMachines: Record = {}; + 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 viewModels: Record> = {}; + 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 Array<{ + name: string; + type: string; + }>; + const inst = vm.instance?.() as any; + const props: Record = {}; + for (const p of properties) { + if (p.type === 'viewModel' && inst) { + try { + const nested = inst.viewModel?.(p.name); + const refName = nested?.getViewModelName?.(); + props[p.name] = refName ? `viewModel:${refName}` : 'viewModel'; + } catch { + props[p.name] = 'viewModel'; + } + } else if (p.type === 'enumType' && inst) { + try { + const ep = inst.enum?.(p.name); + const values: string[] = ep?.values ?? []; + props[p.name] = + values.length > 0 ? `enum:${values.join('|')}` : 'enum'; + } catch { + props[p.name] = 'enum'; + } + } else { + props[p.name] = p.type; + } + } + viewModels[vm.name] = props; + } + + return { + artboards, + defaultArtboard: artboards[0] ?? '', + stateMachines, + viewModels, + }; +} + +// With prettier quoteProps:"consistent", if any key in an object needs quotes, all get quotes. +const IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; +const needsQuote = (s: string) => !IDENTIFIER_RE.test(s); +const quoteKey = (s: string, forceQuote: boolean) => + forceQuote || needsQuote(s) ? `'${s}'` : s; + +function smRecord(stateMachines: Record): string { + const keys = Object.keys(stateMachines); + const force = keys.some(needsQuote); + return Object.entries(stateMachines) + .map(([ab, sms]) => { + const union = sms.length ? sms.map((s) => `'${s}'`).join(' | ') : 'never'; + return ` ${quoteKey(ab, force)}: ${union};`; + }) + .join('\n'); +} + +function vmRecord(viewModels: Record>): string { + const vmKeys = Object.keys(viewModels); + const forceVmKeys = vmKeys.some(needsQuote); + return Object.entries(viewModels) + .map(([vmName, props]) => { + const propKeys = Object.keys(props); + const forcePropKeys = propKeys.some(needsQuote); + const propLines = Object.entries(props) + .map( + ([propName, propType]) => + ` ${quoteKey(propName, forcePropKeys)}: '${propType}';` + ) + .join('\n'); + return ` ${quoteKey(vmName, forceVmKeys)}: {\n${propLines}\n };`; + }) + .join('\n'); +} + +function schemaBody(schema: Schema): string { + const vmSection = + Object.keys(schema.viewModels).length > 0 + ? `\n viewModels: {\n${vmRecord(schema.viewModels)}\n };` + : ''; + return `\ + artboards: ${schema.artboards.map((a) => `'${a}'`).join(' | ')}; + defaultArtboard: '${schema.defaultArtboard}'; + stateMachines: { +${smRecord(schema.stateMachines)} + };${vmSection}`; +} + +function dtsContent(input: string, schema: Schema): string { + return `\ +// Generated by rive-gen-types — do not edit manually. @generated +// eslint-disable +// Source: ${basename(input)} +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ +${schemaBody(schema)} +}>; +export default asset; +`; +} + +function standaloneContent( + input: string, + typeName: string, + schema: Schema +): string { + return `\ +// Generated by rive-gen-types — do not edit manually. +// Source: ${input} +import type { RiveFileSchema } from '@rive-app/react-native'; + +export type ${typeName} = RiveFileSchema & { +${schemaBody(schema)} +}; +`; +} + +async function generate( + input: string, + outPath: string, + mode: 'dts' | 'standalone', + typeName?: string +) { + let schema: Schema; + try { + schema = await extractSchema(input); + } catch (err) { + process.stderr.write( + `Failed to extract schema from ${input}: ${err instanceof Error ? err.message : String(err)}\n` + ); + process.exit(1); + } + + if (!schema.artboards?.length) { + process.stderr.write(`No artboards found in ${input}.\n`); + process.exit(1); + } + + const content = + mode === 'dts' + ? dtsContent(input, schema) + : standaloneContent(input, typeName!, schema); + + mkdirSync(dirname(outPath), { recursive: true }); + writeFileSync(outPath, content, 'utf8'); + process.stdout.write(`Written: ${outPath}\n`); +} + +function findRivFiles(dir: string): string[] { + const results: string[] = []; + for (const entry of readdirSync(dir)) { + const full = resolve(dir, entry); + if (statSync(full).isDirectory()) { + results.push(...findRivFiles(full)); + } else if (extname(entry) === '.riv') { + results.push(full); + } + } + return results; +} + +// --- CLI --- + +async function main() { + // noUncheckedIndexedAccess: slice gives string[], index access gives string | undefined + const args: string[] = process.argv.slice(2); + + if (args[0] === '--all') { + const dir: string | undefined = args[1]; + if (!dir) { + process.stderr.write('Usage: rive-gen-types --all \n'); + process.exit(1); + } + const files = findRivFiles(resolve(process.cwd(), dir)); + if (!files.length) { + process.stderr.write(`No .riv files found in ${dir}\n`); + process.exit(1); + } + for (const file of files) { + await generate(file, `${file}.d.ts`, 'dts'); + } + return; + } + + if (!args.length || args[0]!.startsWith('--')) { + process.stderr.write( + 'Usage:\n' + + ' rive-gen-types # writes .riv.d.ts\n' + + ' rive-gen-types --out # standalone schema .ts\n' + + ' rive-gen-types --all # all .riv files in dir\n' + ); + process.exit(1); + } + + const input = args[0]!; + const outIdx = args.indexOf('--out'); + + if (outIdx !== -1) { + // Standalone mode: generate a named schema type, not a .d.ts + const outPath = resolve(process.cwd(), args[outIdx + 1]!); + const baseName = basename(input, '.riv').replace(/[^a-zA-Z0-9]/g, '_'); + const nameIdx = args.indexOf('--name'); + const typeName = + nameIdx !== -1 + ? args[nameIdx + 1]! + : baseName.charAt(0).toUpperCase() + baseName.slice(1) + 'Schema'; + await generate(input, outPath, 'standalone', typeName); + } else { + if (input.startsWith('http://') || input.startsWith('https://')) { + process.stderr.write( + `Error: URL inputs require --out to specify the output path.\n` + + ` Example: rive-gen-types ${input} --out ./assets/file.riv.d.ts\n` + ); + process.exit(1); + } + // Default: write .riv.d.ts next to the source file + const absInput = resolve(process.cwd(), input); + await generate(input, `${absInput}.d.ts`, 'dts'); + } +} + +main().catch((err: Error) => { + process.stderr.write(err.message + '\n'); + process.exit(1); +}); diff --git a/src/__tests__/typed-rive.test-d.ts b/src/__tests__/typed-rive.test-d.ts new file mode 100644 index 00000000..b62232ef --- /dev/null +++ b/src/__tests__/typed-rive.test-d.ts @@ -0,0 +1,274 @@ +import { expectType, expectError, expectAssignable } from 'tsd'; +import type { TypedRiveFile, RiveAsset } from '../../src/core/TypedRiveFile'; +import type { + TypedViewModelInstance, + TypedViewModelListProperty, + TypedViewModelEnumProperty, + UntypedViewModelInstance, +} from '../../src/core/TypedViewModelInstance'; +import type { + ViewModelInstance, + ViewModelNumberProperty, + ViewModelTriggerProperty, + ViewModelBooleanProperty, +} from '../../src/specs/ViewModel.nitro'; +import type { UseRivePropertyResult } from '../../src/types'; +import { useRiveNumber } from '../../src/hooks/useRiveNumber'; +import type { RiveViewProps } from '../../src/core/RiveView'; +import gradientBorderRiv from '../../example/assets/rive/GradientBorder.riv'; +import blinkoRiv from '../../example/assets/rive/blinko.riv'; + +// Infer schemas from the generated .riv.d.ts assets +type GradientBorderSchema = typeof gradientBorderRiv extends RiveAsset + ? T + : never; +type BlinkoSchema = typeof blinkoRiv extends RiveAsset ? T : never; + +declare const gradientFile: TypedRiveFile; +declare const blinkoFile: TypedRiveFile; +declare const untypedFile: TypedRiveFile; + +// --- TypedRiveFile assignability --- + +// Typed files are assignable to the untyped base (backward compat) +expectAssignable(gradientFile); +expectAssignable(blinkoFile); + +// --- Single-artboard: artboardName constraints --- + +expectAssignable>({ + file: gradientFile, + artboardName: 'Layout', +}); + +// Wrong artboard name must error +expectError>({ + file: gradientFile, + artboardName: 'NotAnArtboard', +}); + +// --- Single-artboard: stateMachineName constraints --- + +expectAssignable>({ + file: gradientFile, + stateMachineName: 'State', +}); + +// Wrong SM name must error +expectError>({ + file: gradientFile, + stateMachineName: 'NonexistentSM', +}); + +// --- Multi-artboard: default artboard (A not specified) --- +// When artboardName is not specified, A defaults to defaultArtboard ('Main'). +// artboardName must be 'Main' (or undefined), stateMachineName constrained to Main's SMs. + +expectAssignable>({ + file: blinkoFile, + artboardName: 'Main', + stateMachineName: 'State Machine 1', +}); + +expectAssignable>({ + file: blinkoFile, + stateMachineName: 'State Machine 1', +}); + +// Non-default artboard without explicit A type parameter must error +expectError>({ + file: blinkoFile, + artboardName: 'StoreItem', +}); + +// Invalid SM on default artboard must error +expectError>({ + file: blinkoFile, + stateMachineName: 'Nonexistent SM', +}); + +// --- Multi-artboard: explicit non-default artboard (A specified) --- +// Specifying A constrains artboardName and stateMachineName to that artboard. + +expectAssignable>({ + file: blinkoFile, + artboardName: 'StoreItem', + stateMachineName: 'State Machine 1', +}); + +// Wrong artboard when A = 'StoreItem' must error +expectError>({ + file: blinkoFile, + artboardName: 'Puck', +}); + +// Wrong SM when A = 'StoreItem' must error +expectError>({ + file: blinkoFile, + artboardName: 'StoreItem', + stateMachineName: 'Nonexistent SM', +}); + +// --- Untyped file: accepts any string (backward compat) --- + +expectAssignable({ + file: untypedFile, + artboardName: 'anything', + stateMachineName: 'anything', +}); + +// --- RiveAsset branding --- + +declare const gradientAsset: RiveAsset; + +// RiveAsset is a branded number +expectType( + gradientAsset +); + +// RiveAsset with wrong schema is not assignable to a different typed asset +expectError>(gradientAsset); + +// ============================================================ +// TypedViewModelInstance +// ============================================================ + +// Blinko storeVM: xbuttonClick:trigger, multiplierValue:number, +// storeOpen:boolean, 'property of pegVM':viewModel:pegVM +// pegVM: blink:trigger, multiplierValue:number, pegType:enumType + +type StoreVMInstance = TypedViewModelInstance; +type PegVMInstance = TypedViewModelInstance; + +declare const storeVM: StoreVMInstance; + +// --- Direct property access --- + +// Valid number property +expectAssignable( + storeVM.numberProperty('multiplierValue') +); + +// Valid trigger property +expectAssignable( + storeVM.triggerProperty('xbuttonClick') +); + +// Valid boolean property +expectAssignable( + storeVM.booleanProperty('storeOpen') +); + +// Wrong kind: xbuttonClick is 'trigger', not 'number' +expectError(storeVM.numberProperty('xbuttonClick')); + +// Nonexistent property name must error +expectError(storeVM.numberProperty('doesNotExist')); + +// --- Nested viewModel access --- + +// Accessing a viewModel reference returns the correct typed instance +expectAssignable( + storeVM.viewModel('property of pegVM') +); + +// Non-viewModel property name is rejected for viewModel() +expectError(storeVM.viewModel('xbuttonClick')); + +// Nonexistent path is rejected +expectError(storeVM.viewModel('notAProperty')); + +// --- Chained nested access --- + +// storeVM → pegVM → pegVM.numberProperty('multiplierValue') +expectAssignable( + storeVM.viewModel('property of pegVM')?.numberProperty('multiplierValue') +); + +// storeVM → pegVM → wrong property kind errors +expectError( + storeVM.viewModel('property of pegVM')?.numberProperty('blink') +); + +// storeVM → pegVM → nonexistent property errors +expectError( + storeVM.viewModel('property of pegVM')?.stringProperty('doesNotExist') +); + +// --- Enum property --- + +// pegVM.pegType is 'enum:normal|multiplier' — returns typed enum property +expectAssignable< + TypedViewModelEnumProperty<'normal' | 'multiplier'> | undefined +>(storeVM.viewModel('property of pegVM')?.enumProperty('pegType')); + +// The enum value type is exactly 'normal' | 'multiplier' +expectType | undefined>( + storeVM.viewModel('property of pegVM')?.enumProperty('pegType') +); + +// Non-enum property rejected for enumProperty() +expectError(storeVM.enumProperty('xbuttonClick')); + +// Nonexistent property rejected +expectError(storeVM.enumProperty('doesNotExist')); + +// --- List property --- + +// storeVM.items is a 'list' — returns a typed list property +expectAssignable | undefined>( + storeVM.listProperty('items') +); + +// Non-list property rejected for listProperty() +expectError(storeVM.listProperty('xbuttonClick')); + +// List element is a union of all file ViewModels (any one of them) +type AnyBlinkoVM = TypedViewModelInstance< + BlinkoSchema, + Extract +>; +declare const list: TypedViewModelListProperty; +expectAssignable>(list.getInstanceAtAsync(0)); + +// ============================================================ +// useRiveNumber +// ============================================================ + +declare const untypedInstance: UntypedViewModelInstance; +declare const plainInstance: ViewModelInstance; + +// Typed overload: valid direct number path → result typed as number +expectType>( + useRiveNumber('multiplierValue', storeVM) +); + +// Typed overload: valid nested number path (nested ViewModel) +expectType>( + useRiveNumber('property of pegVM/multiplierValue', storeVM) +); + +// Typed overload: wrong kind — xbuttonClick is a trigger, not a number +expectError(useRiveNumber('xbuttonClick', storeVM)); + +// Typed overload: nonexistent path +expectError(useRiveNumber('doesNotExist', storeVM)); + +// Typed overload: nested path with wrong property kind +expectError(useRiveNumber('property of pegVM/blink', storeVM)); + +// Typed instance rejected by untyped overload → forces typed overload → wrong path errors +expectError(useRiveNumber('multiplierValue' as string, storeVM)); + +// Untyped overload: plain ViewModelInstance accepts any string path +expectType>( + useRiveNumber('any/path', untypedInstance) +); + +// Untyped overload: plain ViewModelInstance (no brand) also accepted +expectType>( + useRiveNumber('any/path', plainInstance) +); + +// No instance: falls back to untyped overload, still returns number result +expectType>(useRiveNumber('multiplierValue')); diff --git a/src/core/RiveFile.ts b/src/core/RiveFile.ts index ba8e873f..1c991abe 100644 --- a/src/core/RiveFile.ts +++ b/src/core/RiveFile.ts @@ -1,8 +1,6 @@ import { NitroModules } from 'react-native-nitro-modules'; -import type { - RiveFile, - RiveFileFactory as RiveFileFactoryInternal, -} from '../specs/RiveFile.nitro'; +import type { RiveFileFactory as RiveFileFactoryInternal } from '../specs/RiveFile.nitro'; +import type { RiveAsset, RiveFileSchema, TypedRiveFile } from './TypedRiveFile'; import { Image } from 'react-native'; import type { ResolvedReferencedAssets } from './ReferencedAssets'; @@ -15,76 +13,52 @@ const RiveFileInternal = * Provides static methods to load Rive files from URLs, resources, or raw bytes. */ export namespace RiveFileFactory { - /** - * Creates a RiveFile instance from a URL. - * @param url - The URL of the Rive (.riv) file - * @param loadCdn - Whether to load from CDN (default: true) - * @returns Promise that resolves to a RiveFile instance - */ - export async function fromURL( + export async function fromURL( url: string, referencedAssets: ResolvedReferencedAssets | undefined, loadCdn: boolean = true - ): Promise { + ): Promise> { return RiveFileInternal.fromURL( url, loadCdn, referencedAssets ? { data: referencedAssets } : undefined - ); + ) as Promise>; } - /** - * Creates a RiveFile instance from a local file path URL. - * @param pathURL - The local file path of the Rive (.riv) file - * @param loadCdn - Whether to load from CDN (default: true) - * @returns Promise that resolves to a RiveFile instance - */ - export async function fromFileURL( + export async function fromFileURL( fileURL: string, referencedAssets: ResolvedReferencedAssets | undefined = undefined, loadCdn: boolean = true - ): Promise { + ): Promise> { return RiveFileInternal.fromFileURL( fileURL, loadCdn, referencedAssets ? { data: referencedAssets } : undefined - ); + ) as Promise>; } - /** - * Creates a RiveFile instance from a local resource. - * @param resource - The name of the local resource - * @param loadCdn - Whether to load from CDN (default: true) - * @returns Promise that resolves to a RiveFile instance - */ - export async function fromResource( + export async function fromResource( resource: string, referencedAssets: ResolvedReferencedAssets | undefined, loadCdn: boolean = true - ): Promise { + ): Promise> { return RiveFileInternal.fromResource( resource, loadCdn, referencedAssets ? { data: referencedAssets } : undefined - ); + ) as Promise>; } - /** - * Creates a RiveFile instance from raw bytes. - * @param bytes - The raw bytes of the Rive (.riv) file - * @param loadCdn - Whether to load from CDN (default: true) - * @returns Promise that resolves to a RiveFile instance - */ - export async function fromBytes( + export async function fromBytes( bytes: ArrayBuffer, referencedAssets: ResolvedReferencedAssets | undefined, loadCdn: boolean = true - ): Promise { + ): Promise> { return RiveFileInternal.fromBytes( bytes, loadCdn, referencedAssets ? { data: referencedAssets } : undefined - ); + ) as Promise>; } /** @@ -109,11 +83,21 @@ export namespace RiveFileFactory { * config.resolver.assetExts = [...config.resolver.assetExts, 'riv']; * ``` */ + export async function fromSource( + source: RiveAsset, + referencedAssets: ResolvedReferencedAssets | undefined, + loadCdn?: boolean + ): Promise>; export async function fromSource( + source: number | { uri: string }, + referencedAssets: ResolvedReferencedAssets | undefined, + loadCdn?: boolean + ): Promise; + export async function fromSource( source: number | { uri: string }, referencedAssets: ResolvedReferencedAssets | undefined, loadCdn: boolean = true - ): Promise { + ): Promise> { const assetID = typeof source === 'number' ? source : null; const sourceURI = typeof source === 'object' ? source.uri : null; @@ -130,16 +114,24 @@ export namespace RiveFileFactory { try { // handle http address and dev server if (assetURI.match(/https?:\/\//)) { - return RiveFileFactory.fromURL(assetURI, referencedAssets, loadCdn); + return RiveFileFactory.fromURL(assetURI, referencedAssets, loadCdn); } // handle iOS bundled asset if (assetURI.match(/file:\/\//)) { - return RiveFileFactory.fromFileURL(assetURI, referencedAssets, loadCdn); + return RiveFileFactory.fromFileURL( + assetURI, + referencedAssets, + loadCdn + ); } // handle Android bundled asset or resource name uri - return RiveFileFactory.fromResource(assetURI, referencedAssets, loadCdn); + return RiveFileFactory.fromResource( + assetURI, + referencedAssets, + loadCdn + ); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/core/RiveView.tsx b/src/core/RiveView.tsx index f7bd16f3..14df1211 100644 --- a/src/core/RiveView.tsx +++ b/src/core/RiveView.tsx @@ -3,48 +3,35 @@ import { NitroRiveView } from './NitroRiveViewComponent'; import { RiveErrorType, type RiveError } from './Errors'; import { callDispose } from './callDispose'; import type { RiveViewRef } from '../index'; +import type { RiveFileSchema, TypedRiveFile } from './TypedRiveFile'; -export interface RiveViewProps extends Omit< - ComponentProps, - 'onError' +type NitroRiveViewProps = ComponentProps; + +export interface RiveViewProps< + T extends RiveFileSchema = RiveFileSchema, + A extends T['artboards'] = T['defaultArtboard'], +> extends Omit< + NitroRiveViewProps, + 'onError' | 'file' | 'artboardName' | 'stateMachineName' > { onError?: (error: RiveError) => void; + file: TypedRiveFile; + /** Name of the artboard to display. When using a generated schema, only valid artboard names are accepted. */ + artboardName?: A; + /** + * Name of the state machine to play. + * Constrained to the selected artboard's state machines, or the default artboard's if none is specified. + */ + stateMachineName?: T['stateMachines'][A]; } const defaultOnError = (error: RiveError) => console.error(`[${RiveErrorType[error.type]}] ${error.message}`); -/** - * RiveView is a React Native component that renders Rive graphics. - * It provides a seamless way to display and control Rive graphics in your app. - * - * @example - * ```tsx - * - * ``` - * - * @property {RiveFile} file - The Rive file to be displayed - * @property {string} [artboardName] - Name of the artboard to display from the Rive file - * @property {string} [stateMachineName] - Name of the state machine to play - * @property {ViewModelInstance | DataBindMode | DataBindByName} [dataBind] - Data binding configuration for the state machine, defaults to DataBindMode.Auto - * @property {boolean} [autoPlay=true] - Whether to automatically start playing the state machine - * @property {Alignment} [alignment] - How the Rive graphic should be aligned within its container - * @property {Fit} [fit] - How the Rive graphic should fit within its container - * @property {Object} [style] - React Native style object for container customization - * @property {(error: RiveError) => void} [onError] - Callback function that is called when an error occurs - * - * The component also exposes methods for controlling playback: - * - play(): Starts playing the Rive graphic - * - pause(): Pauses the Rive graphic - */ -export function RiveView(props: RiveViewProps) { +export function RiveView< + T extends RiveFileSchema = RiveFileSchema, + A extends T['artboards'] = T['defaultArtboard'], +>(props: RiveViewProps) { const { onError, hybridRef: userHybridRef, ...rest } = props; const wrappedOnError = onError ?? defaultOnError; const viewRef = useRef(null); diff --git a/src/core/TypedRiveFile.ts b/src/core/TypedRiveFile.ts new file mode 100644 index 00000000..82ae93f1 --- /dev/null +++ b/src/core/TypedRiveFile.ts @@ -0,0 +1,64 @@ +import type { RiveFile } from '../specs/RiveFile.nitro'; + +/** + * Describes the statically-known shape of a .riv file. + * Extend this interface via generated schema files to get type-safe artboard names. + * + * @example Generated by: yarn rive-gen-types path/to/file.riv + * ```ts + * export type MySchema = RiveFileSchema & { + * artboards: 'Main' | 'Intro' | 'GameOver'; + * }; + * ``` + */ +export interface RiveFileSchema { + artboards: string; + defaultArtboard: string; + stateMachines: Record; + viewModels: Record>; +} + +/** + * A Metro asset (number) branded with a schema type `T`. + * The `__riveSchema` field is purely phantom — it never exists at runtime. + * + * Created automatically when you `import asset from './file.riv'` and a + * corresponding `file.riv.d.ts` has been generated by `yarn rive-gen-types`. + * + * Pass to `RiveFileFactory.fromSource(asset)` — TypeScript infers `T` automatically. + */ +export type RiveAsset = number & { + readonly __riveSchema?: T; +}; + +/** + * Extracts the RiveFileSchema from a RiveAsset, TypedRiveFile, or a bare RiveFileSchema. + * + * @example + * import rewardsRiv from './rewards.riv'; + * type Schema = SchemaOf; // RewardsSchema + */ +export type SchemaOf = T extends { + readonly __riveSchema?: infer S extends RiveFileSchema; +} + ? S + : T extends { readonly __schema?: infer S extends RiveFileSchema } + ? S + : T extends RiveFileSchema + ? T + : never; + +/** + * A RiveFile branded with a schema type `T`. + * The `__schema` field is purely a phantom type — it never exists at runtime. + * + * Accepts either a `RiveFileSchema` or a `RiveAsset` (i.e. `typeof myRiv`). + * + * Obtain one via `RiveFileFactory.fromURL(...)` or + * `RiveFileFactory.fromSource(typedAsset)`. + */ +export type TypedRiveFile< + T extends RiveFileSchema | RiveAsset = RiveFileSchema, +> = RiveFile & { + readonly __schema?: SchemaOf; +}; diff --git a/src/core/TypedViewModelInstance.ts b/src/core/TypedViewModelInstance.ts new file mode 100644 index 00000000..1230b67c --- /dev/null +++ b/src/core/TypedViewModelInstance.ts @@ -0,0 +1,222 @@ +import type { + ViewModelInstance, + ViewModelNumberProperty, + ViewModelStringProperty, + ViewModelBooleanProperty, + ViewModelColorProperty, + ViewModelTriggerProperty, + ViewModelEnumProperty, + ViewModelImageProperty, + ViewModelListProperty, +} from '../specs/ViewModel.nitro'; +import type { RiveAsset, RiveFileSchema, SchemaOf } from './TypedRiveFile'; + +/** + * A typed list property whose elements are ViewModelInstances from the same file. + * Elements can be any ViewModel defined in the file — the exact type is unknown + * until runtime, so the element type is a union of all file ViewModels. + */ +export interface TypedViewModelListProperty< + T extends RiveFileSchema, +> extends Omit { + getInstanceAtAsync( + index: number + ): Promise< + | TypedViewModelInstance> + | undefined + >; + /** @deprecated Use getInstanceAtAsync instead */ + getInstanceAt( + index: number + ): + | TypedViewModelInstance> + | undefined; +} + +/** Split a pipe-separated string literal into a union: 'a|b|c' → 'a' | 'b' | 'c' */ +type UnionFromPipe = S extends `${infer A}|${infer B}` + ? A | UnionFromPipe + : S; + +/** Extract the enum value union from a schema type string like 'enum:cat|dog|frog' */ +export type EnumValuesOf = S extends `enum:${infer V}` + ? UnionFromPipe + : never; + +/** + * A typed enum property whose value and setter are constrained to the specific enum values + * extracted from the .riv file schema. + */ +export interface TypedViewModelEnumProperty< + Values extends string = string, +> extends Omit< + ViewModelEnumProperty, + 'value' | 'getValueAsync' | 'set' | 'addListener' +> { + /** @deprecated Use getValueAsync (read) or set(value) (write) instead */ + value: Values; + getValueAsync(): Promise; + set(value: Values): void; + addListener(onChanged: (value: Values) => void): () => void; +} + +/** + * Property names whose type matches the given Kind. + * Use kind `'enum'` to match any enum property (stored as `'enum:val1|val2'` in the schema). + */ +export type VMPropsOfKind< + VM extends Record, + Kind extends string, +> = { + [K in keyof VM]: Kind extends 'enum' + ? VM[K] extends `enum:${string}` + ? K + : never + : VM[K] extends Kind + ? K + : never; +}[keyof VM] & + string; + +/** Extract the referenced ViewModel name from 'viewModel:SomeName' */ +export type VMRefName = + TypeStr extends `viewModel:${infer Name}` ? Name : never; + +/** Property names that are viewModel references (type = 'viewModel:*') */ +export type VMRefPropNames> = { + [K in keyof VM]: VM[K] extends `viewModel:${string}` ? K : never; +}[keyof VM] & + string; + +/** Resolved ViewModel name for a nested viewModel reference property */ +type VMRefNameResolved< + T extends RiveFileSchema, + VMName extends keyof T['viewModels'] & string, + P extends keyof T['viewModels'][VMName], +> = VMRefName & + Extract; + +type NestedPathsOfKind< + T extends RiveFileSchema, + VMName extends keyof T['viewModels'] & string, + Kind extends string, +> = { + [P in VMRefPropNames< + T['viewModels'][VMName] + >]: `${P}/${VMPropsOfKind], Kind>}`; +}[VMRefPropNames]; + +/** + * All valid property paths of a given kind, including one level of nested ViewModel paths. + * Direct paths: `'Price_Value'` + * Nested paths: `'Coin/Item_Value'` + */ +export type PathsOfKind< + T extends RiveFileSchema, + VMName extends keyof T['viewModels'] & string, + Kind extends string, +> = + | VMPropsOfKind + | NestedPathsOfKind; + +/** + * A ViewModelInstance typed to a specific ViewModel schema entry. + * Property accessor methods are constrained to valid property names. + * `.viewModel(path)` returns a TypedViewModelInstance for the referenced ViewModel. + * + * Obtain via `useViewModelInstance(typedFile, { viewModelName: 'MyVM' })`. + */ +export interface TypedViewModelInstance< + T extends RiveFileSchema, + VMName extends keyof T['viewModels'] & string, +> extends Omit< + ViewModelInstance, + | 'numberProperty' + | 'stringProperty' + | 'booleanProperty' + | 'colorProperty' + | 'triggerProperty' + | 'enumProperty' + | 'imageProperty' + | 'listProperty' + | 'viewModel' + | 'viewModelAsync' +> { + numberProperty( + path: VMPropsOfKind + ): ViewModelNumberProperty | undefined; + + stringProperty( + path: VMPropsOfKind + ): ViewModelStringProperty | undefined; + + booleanProperty( + path: VMPropsOfKind + ): ViewModelBooleanProperty | undefined; + + colorProperty( + path: VMPropsOfKind + ): ViewModelColorProperty | undefined; + + triggerProperty( + path: VMPropsOfKind + ): ViewModelTriggerProperty | undefined; + + enumProperty

>( + path: P + ): + | TypedViewModelEnumProperty> + | undefined; + + imageProperty( + path: VMPropsOfKind + ): ViewModelImageProperty | undefined; + + listProperty( + path: VMPropsOfKind + ): TypedViewModelListProperty | undefined; + + /** Access a nested ViewModel instance; return type is typed to the referenced ViewModel. */ + viewModel

>( + path: P + ): + | TypedViewModelInstance< + T, + VMRefName & keyof T['viewModels'] & string + > + | undefined; + + viewModelAsync

>( + path: P + ): Promise< + | TypedViewModelInstance< + T, + VMRefName & keyof T['viewModels'] & string + > + | undefined + >; + + /** Brand that prevents typed instances from matching untyped hook overloads. */ + readonly __vmBrand: [T, VMName]; +} + +/** + * A plain ViewModelInstance with no schema type information. + * Used in the untyped hook overloads — typed instances are intentionally + * excluded so TypeScript picks the typed overload when a schema is known. + */ +export type UntypedViewModelInstance = ViewModelInstance & { + __vmBrand?: never; +}; + +/** + * Convenience alias: infer the ViewModel instance type directly from a RiveAsset import. + * + * @example + * import rewardsRiv from './rewards.riv'; + * type RewardsInstance = TypedViewModelOf; + */ +export type TypedViewModelOf< + T extends RiveFileSchema | RiveAsset, + VMName extends Extract['viewModels'], string>, +> = TypedViewModelInstance, VMName>; diff --git a/src/hooks/useRiveBoolean.ts b/src/hooks/useRiveBoolean.ts index 657d9ad6..f6449aa0 100644 --- a/src/hooks/useRiveBoolean.ts +++ b/src/hooks/useRiveBoolean.ts @@ -4,6 +4,12 @@ import { } from '../specs/ViewModel.nitro'; import type { UseRivePropertyResult } from '../types'; import { useRiveProperty } from './useRiveProperty'; +import type { + PathsOfKind, + TypedViewModelInstance, + UntypedViewModelInstance, +} from '../core/TypedViewModelInstance'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; const getBooleanProperty = (vmi: ViewModelInstance, p: string) => vmi.booleanProperty(p); @@ -15,6 +21,17 @@ const getBooleanProperty = (vmi: ViewModelInstance, p: string) => * @param viewModelInstance - The ViewModelInstance containing the boolean property to operate on * @returns An object with the boolean value, a setter function, and an error if the property is not found */ +export function useRiveBoolean< + T extends RiveFileSchema, + N extends Extract, +>( + path: PathsOfKind, + viewModelInstance?: TypedViewModelInstance | null +): UseRivePropertyResult; +export function useRiveBoolean( + path: string, + viewModelInstance?: UntypedViewModelInstance | null +): UseRivePropertyResult; export function useRiveBoolean( path: string, viewModelInstance?: ViewModelInstance | null diff --git a/src/hooks/useRiveColor.ts b/src/hooks/useRiveColor.ts index bd6ce38e..f16ccfd8 100644 --- a/src/hooks/useRiveColor.ts +++ b/src/hooks/useRiveColor.ts @@ -5,6 +5,12 @@ import type { } from '../specs/ViewModel.nitro'; import { useRiveProperty } from './useRiveProperty'; import { RiveColor } from '../core/RiveColor'; +import type { + PathsOfKind, + TypedViewModelInstance, + UntypedViewModelInstance, +} from '../core/TypedViewModelInstance'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; const getColorProperty = (vmi: ViewModelInstance, p: string) => vmi.colorProperty(p); @@ -22,6 +28,17 @@ export interface UseRiveColorResult { * @param viewModelInstance - The ViewModelInstance containing the color property to operate on * @returns An object with the color value as RGBA, a setter function that accepts either RGBA or hex string, and an error if the property is not found */ +export function useRiveColor< + T extends RiveFileSchema, + N extends Extract, +>( + path: PathsOfKind, + viewModelInstance?: TypedViewModelInstance | null +): UseRiveColorResult; +export function useRiveColor( + path: string, + viewModelInstance?: UntypedViewModelInstance | null +): UseRiveColorResult; export function useRiveColor( path: string, viewModelInstance?: ViewModelInstance | null diff --git a/src/hooks/useRiveEnum.ts b/src/hooks/useRiveEnum.ts index ee5d67ba..11ba2b3a 100644 --- a/src/hooks/useRiveEnum.ts +++ b/src/hooks/useRiveEnum.ts @@ -3,18 +3,32 @@ import { type ViewModelInstance, } from '../specs/ViewModel.nitro'; import type { UseRivePropertyResult } from '../types'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; +import { + type EnumValuesOf, + type TypedViewModelInstance, + type UntypedViewModelInstance, + type VMPropsOfKind, +} from '../core/TypedViewModelInstance'; import { useRiveProperty } from './useRiveProperty'; const getEnumProperty = (vmi: ViewModelInstance, p: string) => vmi.enumProperty(p); -/** - * Hook for interacting with enum ViewModel instance properties. - * - * @param path - The path to the enum property - * @param viewModelInstance - The ViewModelInstance containing the enum property to operate on - * @returns An object with the enum value, a setter function, and an error if the property is not found - */ +export function useRiveEnum< + T extends RiveFileSchema, + N extends Extract, + P extends VMPropsOfKind, +>( + path: P, + viewModelInstance?: TypedViewModelInstance | null +): UseRivePropertyResult>; + +export function useRiveEnum( + path: string, + viewModelInstance?: UntypedViewModelInstance | null +): UseRivePropertyResult; + export function useRiveEnum( path: string, viewModelInstance?: ViewModelInstance | null diff --git a/src/hooks/useRiveFile.ts b/src/hooks/useRiveFile.ts index b9d5d83e..ea7cc651 100644 --- a/src/hooks/useRiveFile.ts +++ b/src/hooks/useRiveFile.ts @@ -12,6 +12,11 @@ import type { ReferencedAssets, ResolvedReferencedAssets, } from '../core/ReferencedAssets'; +import type { + RiveAsset, + RiveFileSchema, + TypedRiveFile, +} from '../core/TypedRiveFile'; export type { ReferencedAssets, ResolvedReferencedAssets }; export type RiveFileInput = number | { uri: string } | string | ArrayBuffer; @@ -92,6 +97,17 @@ 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; export function useRiveFile( input: RiveFileInput | undefined, options: UseRiveFileOptions = {} diff --git a/src/hooks/useRiveNumber.ts b/src/hooks/useRiveNumber.ts index 773d30e2..4cda0bb0 100644 --- a/src/hooks/useRiveNumber.ts +++ b/src/hooks/useRiveNumber.ts @@ -4,6 +4,12 @@ import { } from '../specs/ViewModel.nitro'; import type { UseRivePropertyResult } from '../types'; import { useRiveProperty } from './useRiveProperty'; +import type { + PathsOfKind, + TypedViewModelInstance, + UntypedViewModelInstance, +} from '../core/TypedViewModelInstance'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; const getNumberProperty = (vmi: ViewModelInstance, p: string) => vmi.numberProperty(p); @@ -15,6 +21,17 @@ const getNumberProperty = (vmi: ViewModelInstance, p: string) => * @param viewModelInstance - The ViewModelInstance containing the number property to operate on * @returns An object with the number value, a setter function, and an error if the property is not found */ +export function useRiveNumber< + T extends RiveFileSchema, + N extends Extract, +>( + path: PathsOfKind, + viewModelInstance?: TypedViewModelInstance | null +): UseRivePropertyResult; +export function useRiveNumber( + path: string, + viewModelInstance?: UntypedViewModelInstance | null +): UseRivePropertyResult; export function useRiveNumber( path: string, viewModelInstance?: ViewModelInstance | null diff --git a/src/hooks/useRiveString.ts b/src/hooks/useRiveString.ts index e182ddff..c5210e2a 100644 --- a/src/hooks/useRiveString.ts +++ b/src/hooks/useRiveString.ts @@ -4,6 +4,12 @@ import { } from '../specs/ViewModel.nitro'; import type { UseRivePropertyResult } from '../types'; import { useRiveProperty } from './useRiveProperty'; +import type { + PathsOfKind, + TypedViewModelInstance, + UntypedViewModelInstance, +} from '../core/TypedViewModelInstance'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; const getStringProperty = (vmi: ViewModelInstance, p: string) => vmi.stringProperty(p); @@ -15,6 +21,17 @@ const getStringProperty = (vmi: ViewModelInstance, p: string) => * @param viewModelInstance - The ViewModelInstance containing the string property to operate on * @returns An object with the number value, a setter function, and an error if the property is not found */ +export function useRiveString< + T extends RiveFileSchema, + N extends Extract, +>( + path: PathsOfKind, + viewModelInstance?: TypedViewModelInstance | null +): UseRivePropertyResult; +export function useRiveString( + path: string, + viewModelInstance?: UntypedViewModelInstance | null +): UseRivePropertyResult; export function useRiveString( path: string, viewModelInstance?: ViewModelInstance | null diff --git a/src/hooks/useRiveTrigger.ts b/src/hooks/useRiveTrigger.ts index 8f8315c9..9ca2c22d 100644 --- a/src/hooks/useRiveTrigger.ts +++ b/src/hooks/useRiveTrigger.ts @@ -8,6 +8,12 @@ import type { UseViewModelInstanceTriggerParameters, } from '../types'; import { useDisposableMemo } from './useDisposableMemo'; +import type { + PathsOfKind, + TypedViewModelInstance, + UntypedViewModelInstance, +} from '../core/TypedViewModelInstance'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; /** * Hook for interacting with trigger ViewModel instance properties. @@ -21,6 +27,19 @@ import { useDisposableMemo } from './useDisposableMemo'; * @param params - Optional parameters including onTrigger callback * @returns A trigger function and any error */ +export function useRiveTrigger< + T extends RiveFileSchema, + N extends Extract, +>( + path: PathsOfKind, + viewModelInstance?: TypedViewModelInstance | null, + params?: UseViewModelInstanceTriggerParameters +): UseRiveTriggerResult; +export function useRiveTrigger( + path: string, + viewModelInstance?: UntypedViewModelInstance | null, + params?: UseViewModelInstanceTriggerParameters +): UseRiveTriggerResult; export function useRiveTrigger( path: string, viewModelInstance?: ViewModelInstance | null, diff --git a/src/hooks/useViewModelInstance.ts b/src/hooks/useViewModelInstance.ts index f5acea77..007c870a 100644 --- a/src/hooks/useViewModelInstance.ts +++ b/src/hooks/useViewModelInstance.ts @@ -3,6 +3,8 @@ import { useMemo, useRef } from 'react'; import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro'; import type { RiveFile } from '../specs/RiveFile.nitro'; +import type { RiveFileSchema } from '../core/TypedRiveFile'; +import type { TypedViewModelInstance } from '../core/TypedViewModelInstance'; import type { RiveViewRef } from '../index'; import { callDispose } from '../core/callDispose'; import { ArtboardByName } from '../specs/ArtboardBy'; @@ -263,6 +265,19 @@ export type UseViewModelInstanceResult = * if (error) console.error(error.message); * ``` */ +// Typed overload: TypedRiveFile + literal viewModelName → TypedViewModelInstance +export function useViewModelInstance< + T extends RiveFileSchema, + N extends Extract, +>( + source: (RiveFile & { readonly __schema?: T }) | null | undefined, + params: UseViewModelInstanceFileParams & { viewModelName: N } +): + | { instance: TypedViewModelInstance; error: null } + | { instance: null; error: Error } + | { instance: null; error: null } + | { instance: undefined; error: null }; + // RiveFile overloads export function useViewModelInstance( source: RiveFile, diff --git a/src/index.tsx b/src/index.tsx index 7e29e27a..0000d19c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,6 +20,21 @@ export { RiveView, type RiveViewProps } from './core/RiveView'; export type { RiveViewMethods }; export type RiveViewRef = HybridView; export type { RiveFile } from './specs/RiveFile.nitro'; +export type { + RiveAsset, + RiveFileSchema, + SchemaOf, + TypedRiveFile, +} from './core/TypedRiveFile'; +export type { + TypedViewModelInstance, + TypedViewModelOf, + UntypedViewModelInstance, + TypedViewModelListProperty, + TypedViewModelEnumProperty, + PathsOfKind, + EnumValuesOf, +} from './core/TypedViewModelInstance'; export type { ViewModel, ViewModelInstance, diff --git a/tsconfig.json b/tsconfig.json index 278a9b8f..9e098526 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,8 @@ "skipLibCheck": true, "strict": true, "target": "ESNext", - "verbatimModuleSyntax": true + "verbatimModuleSyntax": true, + "allowArbitraryExtensions": true }, - "exclude": ["expo-example", "expo55-example"] + "exclude": ["expo-example", "expo55-example", "**/*.test-d.ts", "scripts/__tests__"] } diff --git a/yarn.lock b/yarn.lock index d8986822..f95ed8cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5459,6 +5459,13 @@ __metadata: languageName: node linkType: hard +"@rive-app/canvas@npm:^2.38.0": + version: 2.38.0 + resolution: "@rive-app/canvas@npm:2.38.0" + checksum: a75508c461db26c48d3d3daebd9bd2b1a28fd528062640bd99f767f3b0fc32a518e3bffaadccbab93b3819c7f1225f7d1c65e1534c036798482cf271b5d61161 + languageName: node + linkType: hard + "@rive-app/react-native@workspace:.": version: 0.0.0-use.local resolution: "@rive-app/react-native@workspace:." @@ -5470,6 +5477,7 @@ __metadata: "@react-native/babel-preset": 0.79.2 "@react-native/eslint-config": ^0.78.0 "@release-it/conventional-changelog": ^9.0.2 + "@rive-app/canvas": ^2.38.0 "@testing-library/react-hooks": ^8.0.1 "@testing-library/react-native": ^13.3.3 "@types/jest": ^29.5.5 @@ -5492,6 +5500,7 @@ __metadata: react-native-nitro-modules: 0.35.0 react-test-renderer: 19.0.0 release-it: ^17.10.0 + tsd: ^0.33.0 turbo: ^1.10.7 typescript: ^5.2.2 peerDependencies: @@ -5828,6 +5837,13 @@ __metadata: languageName: node linkType: hard +"@tsd/typescript@npm:^5.9.2": + version: 5.9.3 + resolution: "@tsd/typescript@npm:5.9.3" + checksum: d1209e850cb36715b62305821f87877c11756cb0530d562f2d0c7f34feb34d6524c13d12f79f020c6bf69d8e3f5085deaef6657004937668c38f4986d82fa98f + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.0": version: 0.10.2 resolution: "@tybys/wasm-util@npm:0.10.2" @@ -5911,20 +5927,30 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.8": - version: 1.0.8 - resolution: "@types/estree@npm:1.0.8" - checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 +"@types/eslint@npm:^7.2.13": + version: 7.29.0 + resolution: "@types/eslint@npm:7.29.0" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: df13991c554954353ce8f3bb03e19da6cc71916889443d68d178d4f858b561ba4cc4a4f291c6eb9eebb7f864b12b9b9313051b3a8dfea3e513dadf3188a77bdf languageName: node linkType: hard -"@types/estree@npm:^1.0.6": +"@types/estree@npm:*, @types/estree@npm:^1.0.6": version: 1.0.9 resolution: "@types/estree@npm:1.0.9" checksum: 752c0afee3ec82b8e24484bf6a27dfa093bbf3de4ef1c20ed0364fb6ad2c0c7971e7504ed9a7aaff103a47e2d945ce7a17f74951743dd944782a0735f53170de languageName: node linkType: hard +"@types/estree@npm:1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -5976,7 +6002,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 @@ -5990,7 +6016,7 @@ __metadata: languageName: node linkType: hard -"@types/minimist@npm:^1.2.2": +"@types/minimist@npm:^1.2.0, @types/minimist@npm:^1.2.2": version: 1.2.5 resolution: "@types/minimist@npm:1.2.5" checksum: 477047b606005058ab0263c4f58097136268007f320003c348794f74adedc3166ffc47c80ec3e94687787f2ab7f4e72c468223946e79892cf0fd9e25e9970a90 @@ -7707,6 +7733,17 @@ __metadata: languageName: node linkType: hard +"camelcase-keys@npm:^6.2.2": + version: 6.2.2 + resolution: "camelcase-keys@npm:6.2.2" + dependencies: + camelcase: ^5.3.1 + map-obj: ^4.0.0 + quick-lru: ^4.0.1 + checksum: 43c9af1adf840471e54c68ab3e5fe8a62719a6b7dbf4e2e86886b7b0ff96112c945736342b837bd2529ec9d1c7d1934e5653318478d98e0cf22c475c04658e2a + languageName: node + linkType: hard + "camelcase-keys@npm:^7.0.0": version: 7.0.2 resolution: "camelcase-keys@npm:7.0.2" @@ -9429,6 +9466,22 @@ __metadata: languageName: node linkType: hard +"eslint-formatter-pretty@npm:^4.1.0": + version: 4.1.0 + resolution: "eslint-formatter-pretty@npm:4.1.0" + dependencies: + "@types/eslint": ^7.2.13 + ansi-escapes: ^4.2.1 + chalk: ^4.1.0 + eslint-rule-docs: ^1.1.5 + log-symbols: ^4.0.0 + plur: ^4.0.0 + string-width: ^4.2.0 + supports-hyperlinks: ^2.0.0 + checksum: e8e0cd3843513fff32a70b036dd349fdab81d73b5e522f23685181c907a1faf2b2ebcae1688dc71d0fc026184011792f7e39b833d349df18fe2baea00d017901 + languageName: node + linkType: hard + "eslint-import-context@npm:^0.1.8": version: 0.1.9 resolution: "eslint-import-context@npm:0.1.9" @@ -9705,6 +9758,13 @@ __metadata: languageName: node linkType: hard +"eslint-rule-docs@npm:^1.1.5": + version: 1.1.235 + resolution: "eslint-rule-docs@npm:1.1.235" + checksum: b163596f9a05568e287b2c78f51a280092122a2e43c45fa2c200f0bd3f61877af186c641dab97620978bec96d9e2cfb621e51728044d9efe42ddc24f5a594b26 + languageName: node + linkType: hard + "eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -11612,6 +11672,13 @@ __metadata: languageName: node linkType: hard +"hosted-git-info@npm:^2.1.4": + version: 2.8.9 + resolution: "hosted-git-info@npm:2.8.9" + checksum: c955394bdab888a1e9bb10eb33029e0f7ce5a2ac7b3f158099dc8c486c99e73809dca609f5694b223920ca2174db33d32b12f9a2a47141dc59607c29da5a62dd + languageName: node + linkType: hard + "hosted-git-info@npm:^4.0.1": version: 4.1.0 resolution: "hosted-git-info@npm:4.1.0" @@ -11900,6 +11967,13 @@ __metadata: languageName: node linkType: hard +"irregular-plurals@npm:^3.2.0": + version: 3.5.0 + resolution: "irregular-plurals@npm:3.5.0" + checksum: 5b663091dc89155df7b2e9d053e8fb11941a0c4be95c4b6549ed3ea020489fdf4f75ea586c915b5b543704252679a5a6e8c6c3587da5ac3fc57b12da90a9aee7 + languageName: node + linkType: hard + "is-absolute@npm:^1.0.0": version: 1.0.0 resolution: "is-absolute@npm:1.0.0" @@ -12686,7 +12760,7 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:^29.7.0": +"jest-diff@npm:^29.0.3, jest-diff@npm:^29.7.0": version: 29.7.0 resolution: "jest-diff@npm:29.7.0" dependencies: @@ -13628,7 +13702,7 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^4.1.0": +"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" dependencies: @@ -13743,7 +13817,7 @@ __metadata: languageName: node linkType: hard -"map-obj@npm:^4.1.0": +"map-obj@npm:^4.0.0, map-obj@npm:^4.1.0": version: 4.3.0 resolution: "map-obj@npm:4.3.0" checksum: fbc554934d1a27a1910e842bc87b177b1a556609dd803747c85ece420692380827c6ae94a95cce4407c054fa0964be3bf8226f7f2cb2e9eeee432c7c1985684e @@ -13819,6 +13893,26 @@ __metadata: languageName: node linkType: hard +"meow@npm:^9.0.0": + version: 9.0.0 + resolution: "meow@npm:9.0.0" + dependencies: + "@types/minimist": ^1.2.0 + camelcase-keys: ^6.2.2 + decamelize: ^1.2.0 + decamelize-keys: ^1.1.0 + hard-rejection: ^2.1.0 + minimist-options: 4.1.0 + normalize-package-data: ^3.0.0 + read-pkg-up: ^7.0.1 + redent: ^3.0.0 + trim-newlines: ^3.0.0 + type-fest: ^0.18.0 + yargs-parser: ^20.2.3 + checksum: 99799c47247f4daeee178e3124f6ef6f84bde2ba3f37652865d5d8f8b8adcf9eedfc551dd043e2455cd8206545fd848e269c0c5ab6b594680a0ad4d3617c9639 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -15141,7 +15235,19 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.2": +"normalize-package-data@npm:^2.5.0": + version: 2.5.0 + resolution: "normalize-package-data@npm:2.5.0" + dependencies: + hosted-git-info: ^2.1.4 + resolve: ^1.10.0 + semver: 2 || 3 || 4 || 5 + validate-npm-package-license: ^3.0.1 + checksum: 7999112efc35a6259bc22db460540cae06564aa65d0271e3bdfa86876d08b0e578b7b5b0028ee61b23f1cae9fc0e7847e4edc0948d3068a39a2a82853efc8499 + languageName: node + linkType: hard + +"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.2": version: 3.0.3 resolution: "normalize-package-data@npm:3.0.3" dependencies: @@ -15681,7 +15787,7 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^5.2.0": +"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" dependencies: @@ -15880,6 +15986,15 @@ __metadata: languageName: node linkType: hard +"plur@npm:^4.0.0": + version: 4.0.0 + resolution: "plur@npm:4.0.0" + dependencies: + irregular-plurals: ^3.2.0 + checksum: fea2e903efca67cc5c7a8952fca3db46ae8d9e9353373b406714977e601a5d3b628bcb043c3ad2126c6ff0e73d8020bf43af30a72dd087eff1ec240eb13b90e1 + languageName: node + linkType: hard + "pngjs@npm:^3.3.0": version: 3.4.0 resolution: "pngjs@npm:3.4.0" @@ -16173,6 +16288,13 @@ __metadata: languageName: node linkType: hard +"quick-lru@npm:^4.0.1": + version: 4.0.1 + resolution: "quick-lru@npm:4.0.1" + checksum: bea46e1abfaa07023e047d3cf1716a06172c4947886c053ede5c50321893711577cb6119360f810cc3ffcd70c4d7db4069c3cee876b358ceff8596e062bd1154 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -16869,6 +16991,17 @@ __metadata: languageName: node linkType: hard +"read-pkg-up@npm:^7.0.0, read-pkg-up@npm:^7.0.1": + version: 7.0.1 + resolution: "read-pkg-up@npm:7.0.1" + dependencies: + find-up: ^4.1.0 + read-pkg: ^5.2.0 + type-fest: ^0.8.1 + checksum: e4e93ce70e5905b490ca8f883eb9e48b5d3cebc6cd4527c25a0d8f3ae2903bd4121c5ab9c5a3e217ada0141098eeb661313c86fa008524b089b8ed0b7f165e44 + languageName: node + linkType: hard + "read-pkg-up@npm:^8.0.0": version: 8.0.0 resolution: "read-pkg-up@npm:8.0.0" @@ -16880,6 +17013,18 @@ __metadata: languageName: node linkType: hard +"read-pkg@npm:^5.2.0": + version: 5.2.0 + resolution: "read-pkg@npm:5.2.0" + dependencies: + "@types/normalize-package-data": ^2.4.0 + normalize-package-data: ^2.5.0 + parse-json: ^5.0.0 + type-fest: ^0.6.0 + checksum: eb696e60528b29aebe10e499ba93f44991908c57d70f2d26f369e46b8b9afc208ef11b4ba64f67630f31df8b6872129e0a8933c8c53b7b4daf0eace536901222 + languageName: node + linkType: hard + "read-pkg@npm:^6.0.0": version: 6.0.0 resolution: "read-pkg@npm:6.0.0" @@ -17165,7 +17310,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.2": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.2": version: 1.22.12 resolution: "resolve@npm:1.22.12" dependencies: @@ -17204,7 +17349,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.11#~builtin, resolve@patch:resolve@^1.22.2#~builtin": +"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.11#~builtin, resolve@patch:resolve@^1.22.2#~builtin": version: 1.22.12 resolution: "resolve@patch:resolve@npm%3A1.22.12#~builtin::version=1.22.12&hash=c3c19d" dependencies: @@ -17496,6 +17641,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:2 || 3 || 4 || 5": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: fb4ab5e0dd1c22ce0c937ea390b4a822147a9c53dbd2a9a0132f12fe382902beef4fbf12cf51bb955248d8d15874ce8cd89532569756384f994309825f10b686 + languageName: node + linkType: hard + "semver@npm:7.6.3, semver@npm:~7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" @@ -18517,6 +18671,13 @@ __metadata: languageName: node linkType: hard +"trim-newlines@npm:^3.0.0": + version: 3.0.1 + resolution: "trim-newlines@npm:3.0.1" + checksum: b530f3fadf78e570cf3c761fb74fef655beff6b0f84b29209bac6c9622db75ad1417f4a7b5d54c96605dcd72734ad44526fef9f396807b90839449eb543c6206 + languageName: node + linkType: hard + "trim-newlines@npm:^4.0.2": version: 4.1.1 resolution: "trim-newlines@npm:4.1.1" @@ -18571,6 +18732,23 @@ __metadata: languageName: node linkType: hard +"tsd@npm:^0.33.0": + version: 0.33.0 + resolution: "tsd@npm:0.33.0" + dependencies: + "@tsd/typescript": ^5.9.2 + eslint-formatter-pretty: ^4.1.0 + globby: ^11.0.1 + jest-diff: ^29.0.3 + meow: ^9.0.0 + path-exists: ^4.0.0 + read-pkg-up: ^7.0.0 + bin: + tsd: dist/cli.js + checksum: 2916bcfd9d46bbeaab09e28ca714d4792c64fe12eda1a31e3d0786546fe93a1d2aeba0af2a68be4f9b211d3466ecf4f0187e4dbe45fe9f3eb29c4690c98e3379 + languageName: node + linkType: hard + "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -18683,6 +18861,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.18.0": + version: 0.18.1 + resolution: "type-fest@npm:0.18.1" + checksum: e96dcee18abe50ec82dab6cbc4751b3a82046da54c52e3b2d035b3c519732c0b3dd7a2fa9df24efd1a38d953d8d4813c50985f215f1957ee5e4f26b0fe0da395 + languageName: node + linkType: hard + "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -18690,6 +18875,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.6.0": + version: 0.6.0 + resolution: "type-fest@npm:0.6.0" + checksum: b2188e6e4b21557f6e92960ec496d28a51d68658018cba8b597bd3ef757721d1db309f120ae987abeeda874511d14b776157ff809f23c6d1ce8f83b9b2b7d60f + languageName: node + linkType: hard + "type-fest@npm:^0.7.1": version: 0.7.1 resolution: "type-fest@npm:0.7.1" @@ -18697,6 +18889,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.8.1": + version: 0.8.1 + resolution: "type-fest@npm:0.8.1" + checksum: d61c4b2eba24009033ae4500d7d818a94fd6d1b481a8111612ee141400d5f1db46f199c014766b9fa9b31a6a7374d96fc748c6d688a78a3ce5a33123839becb7 + languageName: node + linkType: hard + "type-fest@npm:^1.0.1, type-fest@npm:^1.2.1, type-fest@npm:^1.2.2": version: 1.4.0 resolution: "type-fest@npm:1.4.0" @@ -19640,7 +19839,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.9": +"yargs-parser@npm:^20.2.3, yargs-parser@npm:^20.2.9": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3