feat: generate referenced asset names in .riv schemas, type referencedAssets - #359
Open
mfazekas wants to merge 15 commits into
Open
feat: generate referenced asset names in .riv schemas, type referencedAssets#359mfazekas wants to merge 15 commits into
mfazekas wants to merge 15 commits into
Conversation
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
force-pushed
the
feat/type-safe-riv-schema-2
branch
from
August 11, 2026 20:20
c7ba677 to
6cb4e5b
Compare
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
force-pushed
the
feat/riv-schema-asset-names
branch
from
August 12, 2026 04:00
e7b8a40 to
09b3895
Compare
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.
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
assetsrecord — unique asset identifier (the documentedreferencedAssetskey 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 WASMload()asset-loader callback — the only place asset metadata is visible — so no extra pass.On the typing side,
useRiveFile(andRiveFileFactory.fromSource) constrainreferencedAssetsfor schema-typed inputs:typemust match the asset's actual kind (mislabeling a font asimageis a compile error — useful since the new runtime can't infer types and relies on this hint),RiveImageobjects are only accepted for image assets,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 forviewModelName— caught here by sabotage-testing the example before writing the types). Unknown schemas and plain inputs degrade to the untypedReferencedAssetsexactly as before.The out-of-band example now uses the typed asset import as a live showcase;
out_of_band.riv.d.tsshows 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 forclassifyAsset/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):
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.Type '"image"' is not assignable to type '"font"'.RiveImagefor a non-image asset:Type 'RiveImage' is not assignable to type 'ReferencedAssetSource & { type?: "font" }'.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 plainnevergave an unexplained "not assignable to never").Backward compat is pinned by tsd for every untyped input form — url string,
{ uri },require()(any), and plainnumber— all keep accepting arbitrary asset keys.