Skip to content

feat: generate referenced asset names in .riv schemas, type referencedAssets - #359

Open
mfazekas wants to merge 15 commits into
mainfrom
feat/riv-schema-asset-names
Open

feat: generate referenced asset names in .riv schemas, type referencedAssets#359
mfazekas wants to merge 15 commits into
mainfrom
feat/riv-schema-asset-names

Conversation

@mfazekas

@mfazekas mfazekas commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #352 (git-town: main → feat/type-safe-riv-schema-2 → this). Implements the review feature request: generate asset names (images, fonts, audio) for out-of-band handling.

Generated schemas gain an assets record — unique asset identifier (the documented referencedAssets key form, e.g. 'Inter-594377') → 'image' | 'font' | 'audio'. Only non-embedded assets are listed: referenced and CDN-hosted ones are exactly what an app must supply on the new runtime, which doesn't fetch CDN assets (embedded assets need nothing, so they're omitted). Extraction happens in the existing WASM load() asset-loader callback — the only place asset metadata is visible — so no extra pass.

On the typing side, useRiveFile (and RiveFileFactory.fromSource) constrain referencedAssets for schema-typed inputs:

  1. keys must name an asset in the file,
  2. each entry's declared type must match the asset's actual kind (mislabeling a font as image is a compile error — useful since the new runtime can't infer types and relies on this hint),
  3. RiveImage objects are only accepted for image assets,
  4. files with no referenced assets reject all keys.

useRiveFile's typed/untyped overload pair is merged into one generic signature so an invalid key is a hard error instead of silently falling through to the untyped number-input overload (the same fallthrough class fixed in #355 for viewModelName — caught here by sabotage-testing the example before writing the types). Unknown schemas and plain inputs degrade to the untyped ReferencedAssets exactly as before.

The out-of-band example now uses the typed asset import as a live showcase; out_of_band.riv.d.ts shows the emitted shape (3 referenced + 3 CDN assets). All 31 committed schemas regenerated. Coverage: tsd pins for every mistake class plus backward-compat, bun unit tests for classifyAsset/emit, and the emit fuzzer extended with hostile asset names.

Runtime behavior is unchanged (pure codegen + types); semantics target the new runtime per the request — legacy is out of scope.

Verified compiler errors per mistake class (captured from a real probe against the repo tsconfig):

  1. Unknown key: TS2353: Object literal may only specify known properties, and ''Inter-59437'' does not exist in type '{ "Inter-594377"?: …' — the valid keys are listed in the error and autocomplete in the editor.
  2. Wrong asset type: Type '"image"' is not assignable to type '"font"'.
  3. RiveImage for a non-image asset: Type 'RiveImage' is not assignable to type 'ReferencedAssetSource & { type?: "font" }'.
  4. Entry for a file with no referenced assets: Type '{ source: number; type: string; }' is not assignable to type '"this .riv file has no referenced (out-of-band) assets"' — the rejection type is a literal message so the error explains itself (a plain never gave an unexplained "not assignable to never").

Backward compat is pinned by tsd for every untyped input form — url string, { uri }, require() (any), and plain number — all keep accepting arbitrary asset keys.

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.
…-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.
…RefTypeString

rive-wasm (as of 2.39.0) doesn't expose the referenced ViewModel's name on
getProperties() the way it does enumName, so both scripts instantiated a
default instance and asked the nested instance for its name inline. The
workaround now lives in one documented, unit-tested function shared by both
scripts — when rive-wasm ships property-level viewModelName, the swap is a
one-line change in one place.
Bun stays the default dev runner, but nothing requires it: the scripts are
erasable TypeScript, so Node >= 23.6 (or 22.6+ with
--experimental-strip-types) runs them directly. Two portability fixes:
default-import @rive-app/canvas (CJS named exports are invisible to Node's
ESM loader), and an explicit .ts extension on the cross-script import
(allowImportingTsExtensions, valid with noEmit). The entry-point guard
falls back to an argv comparison where import.meta.main is unavailable
(Node < 24.2). Verified: both scripts produce identical output under bun
and node, all 29 schemas byte-identical.
…citly

The scripts are only ever invoked as 'yarn rive-gen-types', 'bun scripts/…',
or 'node scripts/…' (they aren't executable and aren't shipped as a bin), so
the shebang was a misleading runtime commitment. Both bun and Node >= 22.18
run them; see the usage header.
Stacked on #352. Implements the packaging decision from the review
discussion: the schema generator ships inside @rive-app/react-native as
a `bin`, with `@rive-app/canvas` as an optional (dev-time) peer
dependency.

Usage in an app:

1. `yarn add -D @rive-app/canvas` (one-time; the CLI prints this exact
instruction if missing)
2. `npx rive-gen-types assets/rive/my.riv` (or `--all assets/rive`) —
writes `my.riv.d.ts` next to the asset

The published tool is plain compiled CommonJS (esbuild, target node20,
built during `prepare` into `lib/cli/`, launched via a committed
`cli/rive-gen-types.js` wrapper) — no Bun, no TypeScript runtime, no
wasm download for users who don't generate types.

Verified end to end with `npm pack` + install into a fresh project:
missing-canvas error path (clear hint, exit 1) and successful generation
producing the expected `.riv.d.ts`.

Longer-term home (separate `@rive-app/ts-schema-generator` package or
the future Rive CLI) intentionally stays open — migrating is a
deprecation notice in a future release.
@mfazekas
mfazekas force-pushed the feat/type-safe-riv-schema-2 branch from c7ba677 to 6cb4e5b Compare August 11, 2026 20:20
main is on Node 24 now (#357), which runs TypeScript natively, so nothing
here needs Bun anymore:

- yarn rive-gen-types invokes node; script doc headers updated.
- The script tests move from bun:test to node:test + the expect matchers
  package (already a transitive jest dependency, now declared). Ports:
  beforeAll → before, trailing timeouts → options objects, bun's
  expect(value, message) → node:assert with message, __dirname →
  import.meta.dirname (the files run as ESM), extensionless TS imports get
  explicit extensions, and self-spawns use process.execPath.
- CI: the Setup Bun step is removed, and test:scripts is wired into the
  test job — until now the script tests only ever ran locally.
…dAssets

Adds an assets record to generated schemas: unique asset identifier (the
documented referencedAssets key form, e.g. 'Inter-594377') → 'image' |
'font' | 'audio'. Only non-embedded assets are listed — referenced and
CDN-hosted ones are exactly what an app must supply out-of-band on the new
runtime, which does not fetch CDN assets.

useRiveFile (and RiveFileFactory.fromSource) then constrain referencedAssets
for schema-typed inputs: keys must name an asset in the file, each entry's
declared type must match the asset's actual kind, and RiveImage objects are
only accepted for image assets. useRiveFile's typed/untyped overload pair is
merged into one generic signature so an invalid key errors instead of
silently falling through to the untyped number-input overload; unknown
schemas and plain inputs degrade to the untyped ReferencedAssets as before.

Extraction happens in the existing load() asset-loader callback (the only
place the WASM exposes asset metadata). The out-of-band example now uses the
typed asset import; every mistake class is pinned by tsd tests and the
classifyAsset/emit paths by bun tests, with assets added to the emit fuzzer.
…ed input forms

A referencedAssets entry on a file with no referenced assets errored with an
unexplained 'not assignable to never'; the rejection value is now a literal
message type so the compiler error itself says why. Also pin that every
legacy input form (url string, uri object, require()/any, plain number)
keeps accepting arbitrary asset keys.
Pins the full assets record extracted from out_of_band.riv (3 referenced +
3 CDN, embedded excluded) and that embedded-only files report no assets.
@mfazekas
mfazekas force-pushed the feat/riv-schema-asset-names branch from e7b8a40 to 09b3895 Compare August 12, 2026 04:00
Base automatically changed from feat/type-safe-riv-schema-2 to main August 12, 2026 15:36
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.

1 participant