1313 * const file = await RiveFileFactory.fromSource(gameRiv); // TypedRiveFile<GameSchema> — T inferred
1414 */
1515
16- import { spawnSync } from 'child_process' ;
17- import { writeFileSync , mkdirSync , readdirSync , statSync } from 'fs' ;
16+ import { readFileSync , writeFileSync , mkdirSync , readdirSync , statSync } from 'fs' ;
1817import { dirname , resolve , basename , extname } from 'path' ;
1918import { fileURLToPath } from 'url' ;
19+ import { RuntimeLoader } from '@rive-app/canvas' ;
2020
2121const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
22- const extractorPath = resolve ( __dirname , 'rive-extract-schema.ts' ) ;
22+
23+ // Browser shims required by the @rive -app/canvas WASM runtime.
24+ ( globalThis as any ) . document = {
25+ createElement : ( ) => ( { getContext : ( ) => null } ) ,
26+ } ;
27+ ( globalThis as any ) . Image = class { } ;
28+
29+ // Silence WASM warnings (e.g. "No WebGL support") so they don't pollute output.
30+ console . log = ( ...args : unknown [ ] ) =>
31+ process . stderr . write ( args . join ( ' ' ) + '\n' ) ;
32+ console . warn = ( ...args : unknown [ ] ) =>
33+ process . stderr . write ( args . join ( ' ' ) + '\n' ) ;
2334
2435interface Schema {
2536 artboards : string [ ] ;
@@ -28,32 +39,100 @@ interface Schema {
2839 viewModels : Record < string , Record < string , string > > ;
2940}
3041
31- function extractSchema ( input : string ) : Schema {
32- for ( let attempt = 0 ; attempt < 2 ; attempt ++ ) {
33- try {
34- const result = spawnSync ( 'bun' , [ extractorPath , input ] , {
35- encoding : 'utf8' ,
36- timeout : 30_000 ,
37- } ) ;
38- if ( result . error ) throw result . error ;
39- if ( result . signal )
40- throw new Error (
41- `bun killed by signal ${ result . signal } \n${ result . stderr } `
42- ) ;
43- if ( result . status !== 0 )
44- throw new Error (
45- result . stderr || `bun exited with code ${ result . status } `
46- ) ;
47- if ( ! result . stdout . trim ( ) )
48- throw new Error (
49- `bun exited 0 but produced no output\nstderr: ${ result . stderr || '(empty)' } `
50- ) ;
51- return JSON . parse ( result . stdout ) as Schema ;
52- } catch ( err ) {
53- if ( attempt === 1 ) throw err ;
42+ let runtimeReady : Promise < any > | null = null ;
43+
44+ async function getRuntime ( ) : Promise < any > {
45+ if ( ! runtimeReady ) {
46+ runtimeReady = RuntimeLoader . awaitInstance ( ) . then ( ( runtime ) => {
47+ // On headless Linux (no WebGL) the image-load counter (aa.total/aa.loaded)
48+ // never resolves. Wrap img.decode to fire img.la() via queueMicrotask after
49+ // K() returns so the Promise resolves with the actual file, not null.
50+ const origMRI = ( runtime . renderFactory as any ) . makeRenderImage . bind (
51+ runtime . renderFactory
52+ ) ;
53+ ( runtime . renderFactory as any ) . makeRenderImage = function ( ) {
54+ const img = origMRI ( ) ;
55+ if (
56+ img &&
57+ typeof ( img as any ) . la === 'function' &&
58+ typeof ( img as any ) . decode === 'function'
59+ ) {
60+ const origDecode = ( img as any ) . decode . bind ( img ) ;
61+ ( img as any ) . decode = function ( imgBytes : Uint8Array ) {
62+ origDecode ( imgBytes ) ;
63+ queueMicrotask ( ( ) => ( img as any ) . la ( ) ) ;
64+ } ;
65+ }
66+ return img ;
67+ } ;
68+ return runtime ;
69+ } ) ;
70+ }
71+ return runtimeReady ;
72+ }
73+
74+ async function extractSchema ( input : string ) : Promise < Schema > {
75+ const bytes = input . startsWith ( 'http://' ) || input . startsWith ( 'https://' )
76+ ? new Uint8Array ( await ( await fetch ( input ) ) . arrayBuffer ( ) )
77+ : new Uint8Array ( readFileSync ( input ) ) ;
78+
79+ const runtime = await getRuntime ( ) ;
80+
81+ const assetLoader = new ( runtime as any ) . CustomFileAssetLoader ( {
82+ loadContents : ( asset : any , embeddedBytes : Uint8Array ) => {
83+ if ( embeddedBytes ?. length && asset ?. decode ) {
84+ asset . decode ( embeddedBytes ) ;
85+ }
86+ return true ;
87+ } ,
88+ } ) ;
89+
90+ const riveFile = await runtime . load ( bytes , assetLoader , false ) ;
91+
92+ const artboards : string [ ] = [ ] ;
93+ const stateMachines : Record < string , string [ ] > = { } ;
94+ for ( let i = 0 ; i < riveFile . artboardCount ( ) ; i ++ ) {
95+ const artboard = riveFile . artboardByIndex ( i ) ;
96+ artboards . push ( artboard . name ) ;
97+ const sms : string [ ] = [ ] ;
98+ for ( let j = 0 ; j < artboard . stateMachineCount ( ) ; j ++ ) {
99+ sms . push ( artboard . stateMachineByIndex ( j ) . name ) ;
100+ }
101+ stateMachines [ artboard . name ] = sms ;
102+ }
103+
104+ const viewModels : Record < string , Record < string , string > > = { } ;
105+ const vmCount = ( riveFile as any ) . viewModelCount ( ) as number ;
106+ for ( let i = 0 ; i < vmCount ; i ++ ) {
107+ const vm = ( riveFile as any ) . viewModelByIndex ( i ) ;
108+ const properties = vm . getProperties ( ) as Array < { name : string ; type : string } > ;
109+ const inst = vm . instance ?.( ) as any ;
110+ const props : Record < string , string > = { } ;
111+ for ( const p of properties ) {
112+ if ( p . type === 'viewModel' && inst ) {
113+ try {
114+ const nested = inst . viewModel ?.( p . name ) ;
115+ const refName = nested ?. getViewModelName ?.( ) ;
116+ props [ p . name ] = refName ? `viewModel:${ refName } ` : 'viewModel' ;
117+ } catch {
118+ props [ p . name ] = 'viewModel' ;
119+ }
120+ } else if ( p . type === 'enumType' && inst ) {
121+ try {
122+ const ep = inst . enum ?.( p . name ) ;
123+ const values : string [ ] = ep ?. values ?? [ ] ;
124+ props [ p . name ] = values . length > 0 ? `enum:${ values . join ( '|' ) } ` : 'enum' ;
125+ } catch {
126+ props [ p . name ] = 'enum' ;
127+ }
128+ } else {
129+ props [ p . name ] = p . type ;
130+ }
54131 }
132+ viewModels [ vm . name ] = props ;
55133 }
56- throw new Error ( 'unreachable' ) ;
134+
135+ return { artboards, defaultArtboard : artboards [ 0 ] ?? '' , stateMachines, viewModels } ;
57136}
58137
59138// With prettier quoteProps:"consistent", if any key in an object needs quotes, all get quotes.
@@ -133,15 +212,15 @@ ${schemaBody(schema)}
133212` ;
134213}
135214
136- function generate (
215+ async function generate (
137216 input : string ,
138217 outPath : string ,
139218 mode : 'dts' | 'standalone' ,
140219 typeName ?: string
141220) {
142221 let schema : Schema ;
143222 try {
144- schema = extractSchema ( input ) ;
223+ schema = await extractSchema ( input ) ;
145224 } catch ( err ) {
146225 process . stderr . write (
147226 `Failed to extract schema from ${ input } : ${ err instanceof Error ? err . message : String ( err ) } \n`
@@ -179,58 +258,65 @@ function findRivFiles(dir: string): string[] {
179258
180259// --- CLI ---
181260
182- // noUncheckedIndexedAccess: slice gives string[], index access gives string | undefined
183- const args : string [ ] = process . argv . slice ( 2 ) ;
261+ async function main ( ) {
262+ // noUncheckedIndexedAccess: slice gives string[], index access gives string | undefined
263+ const args : string [ ] = process . argv . slice ( 2 ) ;
184264
185- if ( args [ 0 ] === '--all' ) {
186- const dir : string | undefined = args [ 1 ] ;
187- if ( ! dir ) {
188- process . stderr . write ( 'Usage: rive-gen-types --all <directory>\n' ) ;
189- process . exit ( 1 ) ;
190- }
191- const files = findRivFiles ( resolve ( process . cwd ( ) , dir ) ) ;
192- if ( ! files . length ) {
193- process . stderr . write ( `No .riv files found in ${ dir } \n` ) ;
194- process . exit ( 1 ) ;
195- }
196- for ( const file of files ) {
197- generate ( file , `${ file } .d.ts` , 'dts' ) ;
265+ if ( args [ 0 ] === '--all' ) {
266+ const dir : string | undefined = args [ 1 ] ;
267+ if ( ! dir ) {
268+ process . stderr . write ( 'Usage: rive-gen-types --all <directory>\n' ) ;
269+ process . exit ( 1 ) ;
270+ }
271+ const files = findRivFiles ( resolve ( process . cwd ( ) , dir ) ) ;
272+ if ( ! files . length ) {
273+ process . stderr . write ( `No .riv files found in ${ dir } \n` ) ;
274+ process . exit ( 1 ) ;
275+ }
276+ for ( const file of files ) {
277+ await generate ( file , `${ file } .d.ts` , 'dts' ) ;
278+ }
279+ return ;
198280 }
199- process . exit ( 0 ) ;
200- }
201-
202- if ( ! args . length || args [ 0 ] ! . startsWith ( '--' ) ) {
203- process . stderr . write (
204- 'Usage:\n' +
205- ' rive-gen-types <path-or-url> # writes <file>.riv.d.ts\n' +
206- ' rive-gen-types <path> --out <out.ts> # standalone schema .ts\n' +
207- ' rive-gen-types --all <directory> # all .riv files in dir\n'
208- ) ;
209- process . exit ( 1 ) ;
210- }
211281
212- const input = args [ 0 ] ! ;
213- const outIdx = args . indexOf ( '--out' ) ;
214-
215- if ( outIdx !== - 1 ) {
216- // Standalone mode: generate a named schema type, not a .d.ts
217- const outPath = resolve ( process . cwd ( ) , args [ outIdx + 1 ] ! ) ;
218- const baseName = basename ( input , '.riv' ) . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' ) ;
219- const nameIdx = args . indexOf ( '--name' ) ;
220- const typeName =
221- nameIdx !== - 1
222- ? args [ nameIdx + 1 ] !
223- : baseName . charAt ( 0 ) . toUpperCase ( ) + baseName . slice ( 1 ) + 'Schema' ;
224- generate ( input , outPath , 'standalone' , typeName ) ;
225- } else {
226- if ( input . startsWith ( 'http://' ) || input . startsWith ( 'https://' ) ) {
282+ if ( ! args . length || args [ 0 ] ! . startsWith ( '--' ) ) {
227283 process . stderr . write (
228- `Error: URL inputs require --out to specify the output path.\n` +
229- ` Example: rive-gen-types ${ input } --out ./assets/file.riv.d.ts\n`
284+ 'Usage:\n' +
285+ ' rive-gen-types <path-or-url> # writes <file>.riv.d.ts\n' +
286+ ' rive-gen-types <path> --out <out.ts> # standalone schema .ts\n' +
287+ ' rive-gen-types --all <directory> # all .riv files in dir\n'
230288 ) ;
231289 process . exit ( 1 ) ;
232290 }
233- // Default: write <file>.riv.d.ts next to the source file
234- const absInput = resolve ( process . cwd ( ) , input ) ;
235- generate ( input , `${ absInput } .d.ts` , 'dts' ) ;
291+
292+ const input = args [ 0 ] ! ;
293+ const outIdx = args . indexOf ( '--out' ) ;
294+
295+ if ( outIdx !== - 1 ) {
296+ // Standalone mode: generate a named schema type, not a .d.ts
297+ const outPath = resolve ( process . cwd ( ) , args [ outIdx + 1 ] ! ) ;
298+ const baseName = basename ( input , '.riv' ) . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' ) ;
299+ const nameIdx = args . indexOf ( '--name' ) ;
300+ const typeName =
301+ nameIdx !== - 1
302+ ? args [ nameIdx + 1 ] !
303+ : baseName . charAt ( 0 ) . toUpperCase ( ) + baseName . slice ( 1 ) + 'Schema' ;
304+ await generate ( input , outPath , 'standalone' , typeName ) ;
305+ } else {
306+ if ( input . startsWith ( 'http://' ) || input . startsWith ( 'https://' ) ) {
307+ process . stderr . write (
308+ `Error: URL inputs require --out to specify the output path.\n` +
309+ ` Example: rive-gen-types ${ input } --out ./assets/file.riv.d.ts\n`
310+ ) ;
311+ process . exit ( 1 ) ;
312+ }
313+ // Default: write <file>.riv.d.ts next to the source file
314+ const absInput = resolve ( process . cwd ( ) , input ) ;
315+ await generate ( input , `${ absInput } .d.ts` , 'dts' ) ;
316+ }
236317}
318+
319+ main ( ) . catch ( ( err : Error ) => {
320+ process . stderr . write ( err . message + '\n' ) ;
321+ process . exit ( 1 ) ;
322+ } ) ;
0 commit comments