Skip to content

feat: type-safe .riv schema system - #352

Open
mfazekas wants to merge 6 commits into
mainfrom
feat/type-safe-riv-schema-2
Open

feat: type-safe .riv schema system#352
mfazekas wants to merge 6 commits into
mainfrom
feat/type-safe-riv-schema-2

Conversation

@mfazekas

Copy link
Copy Markdown
Collaborator

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 .riv files 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.ts next to each asset; importing the asset then constrains artboardName/stateMachineName, viewModelName, property paths per kind, and enum value unions across RiveView, useRiveFile, useViewModelInstance, and the useRive* property hooks. See #294 for the full API walkthrough.

Changes relative to #294:

  1. Merged with the experimental-runtime changes: RiveFileFactory keeps getBackend(), the loadCdn deprecation warning and optional loadCdn? signatures, now with the typed generics on top; RiveView keeps the new docs/frameRate while its props are generic over the schema.
  2. Bumped @rive-app/canvas to 2.39.0 and regenerated all schemas — output is byte-identical, so the extractor works with the new runtime as is.
  3. Generated schemas for assets added on main since the branch (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.

@mfazekas
mfazekas force-pushed the feat/type-safe-riv-schema-2 branch from e5be8e3 to 6e823df Compare July 27, 2026 07:03
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.
mfazekas added 3 commits July 27, 2026 14:58
Squash of feat/type-safe-riv-schema (PR #294) re-merged onto main after the
experimental-runtime merge (#134).
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / TypedRiveFile and TypedViewModelInstance type system, plus schema-aware overloads for RiveView, 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.ts files are up to date.
  • Bump @rive-app/canvas to 2.39.0 and regenerate/commit .riv.d.ts schemas 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.

Comment thread scripts/rive-gen-types.ts
Comment thread scripts/rive-gen-types.ts
Comment thread scripts/rive-gen-types.ts
mfazekas added 3 commits July 27, 2026 15:35
…-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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants