feat: type-safe .riv schema system - #352
Open
mfazekas wants to merge 6 commits into
Open
Conversation
mfazekas
force-pushed
the
feat/type-safe-riv-schema-2
branch
from
July 27, 2026 07:03
e5be8e3 to
6e823df
Compare
mfazekas
added a commit
that referenced
this pull request
Jul 27, 2026
Stacked on #352 (git-town: main → feat/type-safe-riv-schema-2 → this). Addresses the findings from a fresh review of the schema system; every finding was reproduced with compile probes before fixing, and each fix is pinned by new tsd tests. Type level (`useViewModelInstance`, `TypedViewModelInstance`): 1. Untyped files no longer dead-end. Everything from `RiveFileFactory.*` / `useRiveFile(require(...))` carries the base `RiveFileSchema`, and the old typed overload turned their property paths into `never` while `__vmBrand` blocked the untyped hooks — existing code that compiles on main failed to compile. All name/path constraints now degrade to `string` when the schema isn't statically known. 2. `viewModelName` actually constrains: a typo on a schema-typed file is a hard error instead of silently falling through to the untyped overload (which disabled all downstream path checking). 3. `required: true` keeps its non-null narrowing on typed calls, and the sync (non-`async`) path stays `@deprecated` for typed calls too. 4. Nested paths: instance accessors and `useRiveEnum` now accept them like the sibling hooks, up to two hops (`'Coin/Property_Of_Item/Item_Selection'`), with enum value unions resolved through the path. 5. List elements are plain `ViewModelInstance` — the previous all-VMs union made every accessor parameter intersect to `never`. Generator/CI: 6. `rive-gen-types --all` was silently validating only a prefix of the assets: image decode without WebGL stalls the WASM `load()` forever, the pending promise drains bun's event loop, and the process exits 0 mid-batch. The loader now claims assets without decoding, each `load()` has a 30s watchdog, and the batch continues past failures and exits non-zero. `click-count.riv.d.ts` is new in this PR — it sat in the skipped tail, so its schema had never been generated and CI didn't notice. 7. Schemas with no ViewModels emit `viewModels: {}` — omitting the key failed the `RiveFileSchema` constraint and silently degraded those assets (7 of them) to fully untyped. 8. Names are escaped in emitted `.d.ts` files; enum values containing `|` fall back to untyped `enum` with a warning; the header uses a working `/* eslint-disable */`; the CI sync check also catches untracked `.riv.d.ts` files.
Stacked on #352 (git-town: main → feat/type-safe-riv-schema-2 → this). Addresses the findings from a fresh review of the schema system; every finding was reproduced with compile probes before fixing, and each fix is pinned by new tsd tests. Type level (`useViewModelInstance`, `TypedViewModelInstance`): 1. Untyped files no longer dead-end. Everything from `RiveFileFactory.*` / `useRiveFile(require(...))` carries the base `RiveFileSchema`, and the old typed overload turned their property paths into `never` while `__vmBrand` blocked the untyped hooks — existing code that compiles on main failed to compile. All name/path constraints now degrade to `string` when the schema isn't statically known. 2. `viewModelName` actually constrains: a typo on a schema-typed file is a hard error instead of silently falling through to the untyped overload (which disabled all downstream path checking). 3. `required: true` keeps its non-null narrowing on typed calls, and the sync (non-`async`) path stays `@deprecated` for typed calls too. 4. Nested paths: instance accessors and `useRiveEnum` now accept them like the sibling hooks, up to two hops (`'Coin/Property_Of_Item/Item_Selection'`), with enum value unions resolved through the path. 5. List elements are plain `ViewModelInstance` — the previous all-VMs union made every accessor parameter intersect to `never`. Generator/CI: 6. `rive-gen-types --all` was silently validating only a prefix of the assets: image decode without WebGL stalls the WASM `load()` forever, the pending promise drains bun's event loop, and the process exits 0 mid-batch. The loader now claims assets without decoding, each `load()` has a 30s watchdog, and the batch continues past failures and exits non-zero. `click-count.riv.d.ts` is new in this PR — it sat in the skipped tail, so its schema had never been generated and CI didn't notice. 7. Schemas with no ViewModels emit `viewModels: {}` — omitting the key failed the `RiveFileSchema` constraint and silently degraded those assets (7 of them) to fully untyped. 8. Names are escaped in emitted `.d.ts` files; enum values containing `|` fall back to untyped `enum` with a warning; the header uses a working `/* eslint-disable */`; the CI sync check also catches untracked `.riv.d.ts` files.
mfazekas
force-pushed
the
feat/type-safe-riv-schema-2
branch
from
July 27, 2026 12:59
9aee375 to
3c3c6e8
Compare
There was a problem hiding this comment.
Pull request overview
Introduces a codegen + type-level “schema” system for .riv assets that surfaces artboard/state machine/ViewModel structure as TypeScript phantom types, enabling compile-time validation of names and property paths across RiveView, useRiveFile, useViewModelInstance, and the useRive* property hooks (with no runtime overhead). It also adds Bun-based generator/extractor scripts, commits regenerated .riv.d.ts files, and enforces schema freshness in CI.
Changes:
- Add
RiveFileSchema/RiveAsset/TypedRiveFileandTypedViewModelInstancetype system, plus schema-aware overloads forRiveView,useRiveFile,useViewModelInstance, and property hooks. - Add Bun scripts to extract/generate schemas (
rive-extract-schema,rive-gen-types) with test coverage, and CI validation that committed.riv.d.tsfiles are up to date. - Bump
@rive-app/canvasto2.39.0and regenerate/commit.riv.d.tsschemas for example assets.
Reviewed changes
Copilot reviewed 26 out of 61 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds locked resolution for @rive-app/canvas@2.39.0. |
| package.json | Adds rive-gen-types script, Bun script tests, and @rive-app/canvas devDependency. |
| tsconfig.json | Enables allowArbitraryExtensions (for .riv imports) and excludes script tests from TS build. |
| eslint.config.mjs | Ignores generated **/*.riv.d.ts files in linting. |
| .github/workflows/ci.yml | Adds CI step to regenerate and verify committed .riv.d.ts schema files are in sync. |
| .github/actions/setup/action.yml | Installs Bun in CI to run schema generator and script tests. |
| src/index.tsx | Re-exports new schema/typed-file/typed-instance type surface from the library entrypoint. |
| src/core/TypedRiveFile.ts | Defines RiveFileSchema, RiveAsset<T> branding, and TypedRiveFile<T> schema propagation. |
| src/core/TypedViewModelInstance.ts | Defines schema-driven path typing, enum value unions, and typed ViewModel instance interfaces. |
| src/core/RiveFile.ts | Adds schema-generic return types to RiveFileFactory methods and schema-carrying overloads for fromSource. |
| src/core/RiveView.tsx | Makes RiveView props generic over schema to constrain artboard/state machine names. |
| src/hooks/useRiveFile.ts | Adds typed overload: passing a generated RiveAsset<T> yields a TypedRiveFile<T>. |
| src/hooks/useViewModelInstance.ts | Adds schema-aware overloads that type instance based on file schema + viewModelName. |
| src/hooks/useRiveNumber.ts | Adds typed overload constraining path to number properties for a typed ViewModel instance. |
| src/hooks/useRiveString.ts | Adds typed overload constraining path to string properties for a typed ViewModel instance. |
| src/hooks/useRiveBoolean.ts | Adds typed overload constraining path to boolean properties for a typed ViewModel instance. |
| src/hooks/useRiveColor.ts | Adds typed overload constraining path to color properties for a typed ViewModel instance. |
| src/hooks/useRiveTrigger.ts | Adds typed overload constraining path to trigger properties for a typed ViewModel instance. |
| src/hooks/useRiveEnum.ts | Adds typed overload constraining path to enum properties and narrowing values to schema unions. |
| src/tests/typed-rive.test-d.ts | Adds tsd type-level tests validating schema propagation and path/name constraints. |
| scripts/rive-extract-schema.ts | Adds Bun CLI to extract .riv schema (artboards/SMs/ViewModels) to JSON. |
| scripts/rive-gen-types.ts | Adds Bun CLI to generate .riv.d.ts schema declarations and standalone schema types. |
| scripts/tests/rive-extract-schema.test.ts | Adds Bun tests for schema extractor behavior (incl. nested refs + enum extraction). |
| scripts/tests/rive-gen-types.test.ts | Adds Bun tests for .riv.d.ts generation next to an asset. |
| scripts/tests/rive-gen-emit.test.ts | Adds emitter/escaping tests ensuring hostile names still produce valid TS. |
| scripts/tests/rive-gen-batch.test.ts | Adds regression test ensuring --all generates for every .riv (no silent truncation). |
| example/src/exercisers/RiveDataBindingExample.tsx | Updates example to import a typed .riv asset and use schema-typed hooks and instances. |
| example/src/exercisers/FileSwitcher.tsx | Adds an exerciser for rapid file switching to stress dispose/render race conditions. |
| example/assets/rive/arbtboards-models-instances.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/artboard_db_test.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/blinko.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/bouncing_ball.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/click-count.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/counter.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/databinding.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/databinding_images.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/databinding_lists.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/fallback_fonts.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/font_fallback.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/GradientBorder.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/hello_world_text.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/inputglow.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/ios_android_layouts_demo_v01.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/layout_test.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/layouts_demo.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/many_viewmodels.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/movecircle.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/nodefaultbouncing.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/on_entry_test.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/out_of_band.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/quick_start.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/rating.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/rewards.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/style_fallback_fonts.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/tabtest.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/viewmodelproperty.riv.d.ts | Generated schema typings for this example asset. |
| example/assets/rive/vm_value_change_test.riv.d.ts | Generated schema typings for this example asset. |
Comments suppressed due to low confidence (1)
scripts/rive-gen-types.ts:344
- This error message suggests using --out with a ".riv.d.ts" path, but --out currently triggers standalone schema generation (not an asset default-export declaration). The example should point to a .ts output (or the CLI should be updated to support emitting .riv.d.ts to an explicit path).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…-out/--name, fix usage docs - fetch() for URL inputs now fails early with the HTTP status instead of handing error-page bytes to the WASM loader. - --out and --name without a value print a clear error instead of crashing on the non-null assertion. - Header and usage text no longer imply URLs write a sibling .riv.d.ts (they require --out), and the fromSource example includes the required referencedAssets argument.
- Seeded property-based fuzz (300 iterations, mulberry32): random schemas over a hostile alphabet (quotes, backslashes, newlines, unicode, the '|' and 'viewModel:' encoding metacharacters) must always emit TS that parses cleanly and round-trips every artboard name exactly. This found a real bug: escapeLiteral missed newlines/control characters, emitting unterminated string literals — now escaped via JSON.stringify. Verified differentially: the fuzzer fails in <10ms on the old escaping. - Corrupt-input test: random bytes and a truncated .riv must fail the batch loudly (per-file 'Failed:' + non-zero exit) while valid files in the same batch still generate. Found that load() resolves null for unparseable bytes — both scripts now throw a clear error instead of 'null is not an object'.
…dd expo typed demo Building an end-user demo in expo-example surfaced another silent fallback: RiveView's generic was constrained to RiveFileSchema only, so a file annotated with the documented TypedRiveFile<typeof asset> pattern failed the constraint, inference fell back to the base schema, and artboard/state-machine name checking silently disappeared (files obtained from useRiveFile inferred fine, which is why the tsd suite missed it). RiveViewProps/RiveView now accept RiveFileSchema | RiveAsset and normalize through SchemaOf, matching TypedRiveFile — pinned with tsd call-site tests. The expo-example demo shows the full user flow: generated .riv.d.ts for its assets, a typed-demo screen using typed RiveView names, viewModelName, nested number paths, and the enum value union, with live values on screen. Verified by sabotage: each of 5 name/path/value typos is a compile error.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #294 (same feature, re-based onto main after the experimental-runtime merge in #134; squashed there since every commit conflicted with the runtime rewrite).
Codegen pipeline that extracts artboard/state machine/ViewModel schemas from
.rivfiles and surfaces them as TypeScript phantom types — no runtime overhead.yarn rive-gen-types example/assets/rive/rewards.riv(or--all) emits a.riv.d.tsnext to each asset; importing the asset then constrainsartboardName/stateMachineName,viewModelName, property paths per kind, and enum value unions acrossRiveView,useRiveFile,useViewModelInstance, and theuseRive*property hooks. See #294 for the full API walkthrough.Changes relative to #294:
RiveFileFactorykeepsgetBackend(), theloadCdndeprecation warning and optionalloadCdn?signatures, now with the typed generics on top;RiveViewkeeps the new docs/frameRatewhile its props are generic over the schema.@rive-app/canvasto 2.39.0 and regenerated all schemas — output is byte-identical, so the extractor works with the new runtime as is.tabtest.riv,arbtboards-models-instances.riv).Note: rive-wasm 2.39.0 now exposes
ViewModelProperty.enumName, so the "enum names not accessible" limitation from #294 is gone — a follow-up can emit named enum types instead of inlined value unions.