feat: type-safe .riv schema system#294
Open
mfazekas wants to merge 24 commits into
Open
Conversation
Adds a codegen pipeline that extracts artboard/SM/ViewModel schemas from .riv files at build time and exposes them as TypeScript phantom types. RiveView, useRiveFile, useRiveNumber/String/Boolean/Color/Trigger, and useViewModelInstance all gain typed overloads so wrong artboard names, state machine names, or property paths are caught at compile time. Scripts: rive-extract-schema.ts (WASM-based extractor), rive-gen-types.ts (generates .riv.d.ts alongside assets), with bun tests for both. Type-level tests via tsd in src/__tests__/typed-rive.test-d.ts.
…lInstance Adds __vmBrand to TypedViewModelInstance so TypeScript rejects passing a typed instance to untyped hook overloads, ensuring wrong property paths are caught at compile time. Also adds allowArbitraryExtensions to tsconfig for .riv imports.
…on headless Linux
…t-schema Patch makeRenderImage to wrap img.decode so img.la() fires via queueMicrotask after the WASM K() returns (calling it synchronously resolves the Promise with null since H hasn't been assigned yet). Also pass actual embedded bytes through CustomFileAssetLoader so fonts and images are decoded, not just marked handled.
…ocess Spawning one bun process per .riv file via spawnSync was flaky under CI load: processes occasionally exited 0 with empty stdout due to resource contention. Now the WASM runtime is initialised once and all files are processed in the same process, eliminating the subprocess entirely.
d3b12da to
a5120ce
Compare
mfazekas
added a commit
that referenced
this pull request
Jul 10, 2026
Replace the session-invented eslint typetest convention with the tsd infrastructure PR #294 introduces, copied as closely as possible so whichever PR merges second reconciles trivially: tsd ^0.33.0, the 'typetest' script (tsd --typings src/index.tsx), the package.json tsd block (directory src/__tests__, paths/moduleResolution/jsx options), jest testPathIgnorePatterns for *.test-d.ts, eslint + root-tsconfig excludes for *.test-d.ts. The pins live in src/__tests__/useViewModelInstance.test-d.ts as explicit assertions: expectDeprecated for every call that must resolve to a deprecated overload (bare, async: false, widened boolean async, exported param types), expectNotDeprecated for the literal async: true forms including the { ...params, async: true } re-pin, expectType for the required-narrowing behavior, and expectError for invalid param combinations. Verified that tsd resolves deprecation per-overload for call expressions (failure messages name the resolved signature). One deliberate divergence from #294: CI gates 'yarn typetest' in the lint job (theirs wires the script without a CI step) — keep the step when the branches meet.
mfazekas
added a commit
that referenced
this pull request
Jul 10, 2026
…: true }) (#304) ## What Adds an `async: true` param to `useViewModelInstance` that creates the ViewModelInstance via the non-deprecated `*Async` runtime APIs, resolving off the JS thread. The result gains an `isLoading` state (the sync path reports it too, so both modes share one result shape). The non-async overloads are deprecated per-overload and `async: true` becomes the default in the next major. ## Why The sync creation path uses `@deprecated` runtime APIs that block the JS thread. Keeping a single hook name (instead of a separate `useViewModelInstanceAsync`) makes migration a param flip, keeps parity with `@rive-app/react`, and lets the default change at the next major without a rename. ## Notes - `async` selects between two hook implementations and must stay constant for a component's lifetime (documented on the param; a dev-mode `console.error` explains a flip before React's hooks-order invariant fires). - Deprecation coverage is total: any call not provably `async: true` — including params objects whose `async` widened to `boolean`, or values of the exported `*Params` types — resolves to a deprecated overload. The message includes the fix: re-pin with `{ ...params, async: true }`. - Overload/deprecation resolution is regression-pinned with tsd (`yarn typetest`, `src/__tests__/useViewModelInstance.test-d.ts` — `expectDeprecated`/`expectNotDeprecated`/`expectError`/`expectType`). The wiring mirrors #294's verbatim (same dep, script, config) so whichever PR merges second reconciles trivially; this PR additionally gates the script in the CI lint job. - In both modes, a `null` source settles terminally while `undefined` keeps loading — mirroring `useRiveFile` and `useRive`. - Companion fixes: friendly artboard/instance not-found errors with the native rejection preserved as `cause` (and swallowed native failures logged); the new backends resolve null for a VM-less default artboard instead of surfacing a raw error (issue #189 fixture); a RiveViewRef source polls briefly for the view's auto-bound instance instead of settling a one-shot null; the intermediate ViewModel wrapper is disposed after creation; on source change the hook resets during render so no frame ever pairs the new source with the previous (about-to-be-disposed) instance; `required` failures throw actionable messages (a null source points at the upstream hook's error). - Legacy Android thread-safety: `getViewModelInstance()` no longer traverses controller state from the JS thread — it reads a main-thread-maintained snapshot (refreshed eagerly when binding changes), avoiding both the #297 race class and a JS↔main `runBlocking` deadlock hazard. - CI: the expo57 harness now runs the full shared suite (`example/__tests__` had been silently dropped — coverage went from 4 to 26 suites) with attempt budgets resized to fit, and crash reports are collected on the legacy iOS job too. - Example screens migrated to `{ async: true }`. ## Breaking changes - `useRive().riveViewRef` is `undefined` (not `null`) until the view is ready; `null` now means failed/detached. Migrate `riveViewRef !== null` checks to `!= null` (or `riveViewRef?.method()`), and widen any explicit `RiveViewRef | null` annotations. - On the sync (non-async, deprecated) path a `null` source now settles to `{ instance: null, isLoading: false }` instead of a perpetual `instance: undefined`, and `required: true` throws for it — matching the async semantics. - `UseViewModelInstanceResult` gained an `isLoading` field (additive for consumers, breaking only for exact-shape type assertions). - Legacy backends: the `*Async` runtime lookups now execute on the main thread — "async" means "doesn't block the JS thread", never "parallel with rendering". ## Verify `yarn tsc`, `yarn lint`, `yarn typetest` (tsd), `yarn jest` (86 passed, incl. committed-frame, overload-compat, and async-flip-guard regression tests). On-device: the full restored harness suite (26 suites / 204 tests) green on all four CI jobs (iOS + Android × experimental + legacy); the legacy `cause` divergence, the VM-less raw error, and the ref-source race were each reproduced before fixing.
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.
Overview
Codegen pipeline that extracts artboard/state machine/ViewModel schemas from
.rivfiles and surfaces them as TypeScript phantom types — no runtime overhead.How it works
Step 1 — generate
.riv.d.tsalongside each asset (committed to git):yarn rive-gen-types example/assets/rive/rewards.riv yarn rive-gen-types --all example/assets/rive/ # batchUses
@rive-app/canvasWASM to introspect the file and emits a declaration:Step 2 — use the typed asset:
What's included
scripts/rive-extract-schema.ts— WASM-based extractor; resolves nested VM refs and extracts enum valuesscripts/rive-gen-types.ts— generates.riv.d.ts, single file or--allbatchsrc/core/TypedRiveFile.ts—RiveAsset<T>,TypedRiveFile<T>,SchemaOf<T>src/core/TypedViewModelInstance.ts—TypedViewModelInstance<T,N>,TypedViewModelOf<T,N>,UntypedViewModelInstance,PathsOfKind,EnumValuesOfRiveView,useRiveFile,useViewModelInstance,useRiveNumber/String/Boolean/Color/Enum/Trigger__vmBrandphantom property onTypedViewModelInstance— prevents typed instances from silently matching untyped hook overloadssrc/__tests__/typed-rive.test-d.ts— tsd type-level tests against real.riv.d.tsfiles.riv.d.tsis out of dateNotes
.riv.d.tsfiles are committed (same as nitrogen-generated bindings); CI enforces they stay in syncallowArbitraryExtensions: truein tsconfig for.rivimports to resolve their.riv.d.tsschemaToJSON()in the C++ runtime would fix this cleanly