@yodaos-pkg/ink 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,648 @@
1
+ # `@yodaos-pkg/ink`
2
+
3
+ `@yodaos-pkg/ink` is the browser-facing Ink SDK for third-party frontend
4
+ projects.
5
+
6
+ It wraps the low-level wasm runtime and `InkWebView` into a stable JavaScript
7
+ and TypeScript API so you can:
8
+
9
+ - initialize the Ink wasm runtime
10
+ - create and destroy an `InkView`
11
+ - bind a `canvas` and forward DOM events such as keyboard and pointer input
12
+ - open an Ink bundle directly from memory
13
+ - fetch and render an Ink bundle from an HTTP VFS service
14
+
15
+ ## Use Cases
16
+
17
+ This package is designed for:
18
+
19
+ - plain browser-based frontend applications
20
+ - React applications embedding Ink inside a component
21
+ - Vue applications embedding Ink inside a page or component
22
+ - frontend apps that fetch Ink resources from a backend VFS service
23
+
24
+ ## What You Need Before Starting
25
+
26
+ Before integrating the SDK, make sure you have one of these two inputs:
27
+
28
+ 1. An in-memory bundle
29
+ - You already have the contents of `app.json`, page scripts, styles, images,
30
+ and other files.
31
+ - This is a good fit when your build pipeline or backend has already prepared
32
+ the bundle.
33
+
34
+ 2. A VFS service URL
35
+ - The frontend only knows an `appId` and an HTTP VFS endpoint.
36
+ - This is a good fit when assets live on the server and the browser fetches
37
+ them on demand.
38
+
39
+ If you use the VFS flow, the server side usually works together with
40
+ `@yodaos-pkg/ink-vfs-server`.
41
+
42
+ ## Installation
43
+
44
+ ### Step 1: Install the SDK
45
+
46
+ ```bash
47
+ npm install @yodaos-pkg/ink
48
+ ```
49
+
50
+ If you also want to use the VFS flow, install the server package as well:
51
+
52
+ ```bash
53
+ npm install @yodaos-pkg/ink-vfs-server
54
+ ```
55
+
56
+ ## Core API
57
+
58
+ These are the most commonly used APIs:
59
+
60
+ ### `createInkView(options)`
61
+
62
+ Creates an `InkView` instance and initializes the wasm runtime internally.
63
+
64
+ **Parameters**
65
+
66
+ - `options.width: number`
67
+ - Required
68
+ - The logical width of the Ink view
69
+ - `options.height: number`
70
+ - Required
71
+ - The logical height of the Ink view
72
+ - `options.scaleFactor?: number`
73
+ - Optional
74
+ - The device pixel ratio, defaulting to `window.devicePixelRatio`
75
+ - `options.canvas?: HTMLCanvasElement`
76
+ - Optional
77
+ - If provided, the view is automatically bound to this canvas
78
+ - `options.wasm?: InitInkOptions`
79
+ - Optional
80
+ - Lets you customize wasm initialization
81
+ - `wasmUrl?: string | URL | Request`
82
+ - `moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module`
83
+
84
+ **What it does**
85
+
86
+ - Initializes the Ink wasm runtime
87
+ - Creates the underlying `InkWebView`
88
+ - Returns the higher-level `InkView` wrapper
89
+ - Automatically calls `bindCanvas()` if a `canvas` is provided
90
+
91
+ **Returns**
92
+
93
+ - `Promise<InkView>`
94
+ - Resolves to an `InkView` instance that can then call
95
+ `bindDomEvents()`, `openBundle()`, and `openFromVfs()`
96
+
97
+ ### `view.bindDomEvents(options?)`
98
+
99
+ Binds common DOM events to the current `InkView`.
100
+
101
+ **Parameters**
102
+
103
+ - `options.canvas?: HTMLCanvasElement`
104
+ - Optional
105
+ - Overrides the currently bound canvas
106
+ - `options.keyboardTarget?: EventTarget | null`
107
+ - Optional
108
+ - The keyboard event target, defaulting to `window` when available
109
+ - `options.focusTarget?: HTMLElement | HTMLCanvasElement | null`
110
+ - Optional
111
+ - The target used for focus and blur events, defaulting to the canvas
112
+ - `options.preventWheelDefault?: boolean`
113
+ - Optional
114
+ - Controls whether wheel default behavior is prevented
115
+ - Defaults to `true`
116
+
117
+ **What it does**
118
+
119
+ - Binds `pointerdown` / `pointermove` / `pointerup` / `pointercancel`
120
+ - Binds `wheel`
121
+ - Binds `keydown` / `keyup`
122
+ - Binds `focus` / `blur`
123
+ - Automatically forwards these browser events into the Ink runtime
124
+
125
+ **Returns**
126
+
127
+ - `() => void`
128
+ - Returns a cleanup function
129
+ - Calling it removes every listener created by this binding call
130
+
131
+ ### `view.openBundle(options)`
132
+
133
+ Opens an Ink bundle that already exists in memory.
134
+
135
+ **Parameters**
136
+
137
+ - `options.appId: string`
138
+ - Required
139
+ - The application ID of the Ink app to open
140
+ - `options.files: BundleFiles`
141
+ - Required
142
+ - Supports three formats:
143
+ - `Map<string, BundleFileInput>`
144
+ - `Array<[string, BundleFileInput]>`
145
+ - `Record<string, BundleFileInput>`
146
+ - `BundleFileInput`
147
+ - Supported input types:
148
+ - `string`
149
+ - `Uint8Array`
150
+ - `ArrayBuffer`
151
+ - `ArrayBufferView`
152
+ - `options.initialPage?: string | null`
153
+ - Optional
154
+ - Sets the first page to open
155
+ - `options.query?: string | Record<string, unknown> | null`
156
+ - Optional
157
+ - Initial query payload
158
+ - If an object is provided, the SDK serializes it into JSON
159
+
160
+ **What it does**
161
+
162
+ - Normalizes the file set into the bundle shape expected by the runtime
163
+ - Calls the underlying `openBundle`
164
+ - Opens the requested Ink app
165
+ - Automatically triggers a render request
166
+
167
+ **Returns**
168
+
169
+ - `InkView`
170
+ - Returns the current instance so it can be chained
171
+
172
+ ### `view.openFromVfs(options)`
173
+
174
+ Fetches an Ink bundle from a remote HTTP VFS service and opens it.
175
+
176
+ **Parameters**
177
+
178
+ - `options.appId: string`
179
+ - Required
180
+ - The application ID to open
181
+ - `options.baseUrl: string`
182
+ - Required
183
+ - The VFS service base URL, for example
184
+ `https://example.com/ink-vfs`
185
+ - `options.fetch?: typeof globalThis.fetch`
186
+ - Optional
187
+ - Lets you provide a custom `fetch` implementation
188
+ - `options.signal?: AbortSignal`
189
+ - Optional
190
+ - Used to cancel the request flow
191
+ - `options.headers?: HeadersInit`
192
+ - Optional
193
+ - Extra request headers
194
+ - `options.initialPage?: string | null`
195
+ - Optional
196
+ - Sets the first page to open
197
+ - `options.query?: string | Record<string, unknown> | null`
198
+ - Optional
199
+ - Initial query payload
200
+
201
+ **What it does**
202
+
203
+ - Fetches the VFS manifest first
204
+ - Fetches each file listed in the manifest
205
+ - Reconstructs the bundle in the browser
206
+ - Calls `openBundle()` automatically
207
+
208
+ **Returns**
209
+
210
+ - `Promise<LoadedVfsBundle>`
211
+ - Resolves to the fetched bundle data:
212
+ - `appId: string`
213
+ - `manifest: VfsManifest`
214
+ - `files: Map<string, Uint8Array>`
215
+
216
+ ### `view.startRendering()`
217
+
218
+ Starts the continuous render loop.
219
+
220
+ **Parameters**
221
+
222
+ - None
223
+
224
+ **What it does**
225
+
226
+ - Enables automatic rendering mode
227
+ - Uses `requestAnimationFrame` internally to keep rendering
228
+ - Works well for animations, interaction-heavy apps, and continuously updating
229
+ UIs
230
+
231
+ **Returns**
232
+
233
+ - `InkView`
234
+ - Returns the current instance so it can be chained
235
+
236
+ ### `view.destroy()`
237
+
238
+ Destroys the current `InkView` and releases its resources.
239
+
240
+ **Parameters**
241
+
242
+ - None
243
+
244
+ **What it does**
245
+
246
+ - Stops the render loop
247
+ - Removes event listeners created by `bindDomEvents()`
248
+ - Calls the underlying `InkWebView.destroy()`
249
+ - Releases runtime resources held by the current view
250
+
251
+ **Returns**
252
+
253
+ - `void`
254
+
255
+ ## Plain JavaScript Integration
256
+
257
+ This is the most direct way to integrate Ink into a plain JavaScript app.
258
+
259
+ ### Step 1: Prepare a container element
260
+
261
+ Start with a `canvas` element:
262
+
263
+ ```html
264
+ <canvas id="ink-root" style="width: 375px; height: 667px;"></canvas>
265
+ ```
266
+
267
+ ### Step 2: Import the SDK
268
+
269
+ ```js
270
+ import { createInkView } from '@yodaos-pkg/ink';
271
+ ```
272
+
273
+ ### Step 3: Prepare the bundle files
274
+
275
+ If your assets are already available in memory, organize them as an object or a
276
+ `Map`:
277
+
278
+ ```js
279
+ const files = {
280
+ 'app.json': JSON.stringify({
281
+ pages: ['pages/index/index'],
282
+ }),
283
+ 'pages/index/index.js': `
284
+ export default {
285
+ data: {
286
+ title: 'Hello Ink'
287
+ }
288
+ };
289
+ `,
290
+ };
291
+ ```
292
+
293
+ ### Step 4: Create the view and bind the canvas
294
+
295
+ ```js
296
+ const canvas = document.getElementById('ink-root');
297
+
298
+ const view = await createInkView({
299
+ width: canvas.clientWidth,
300
+ height: canvas.clientHeight,
301
+ scaleFactor: window.devicePixelRatio,
302
+ canvas,
303
+ });
304
+ ```
305
+
306
+ ### Step 5: Bind DOM events
307
+
308
+ This wires common pointer, keyboard, focus, and blur events automatically:
309
+
310
+ ```js
311
+ view.bindDomEvents();
312
+ ```
313
+
314
+ ### Step 6: Open the Ink bundle
315
+
316
+ ```js
317
+ view.openBundle({
318
+ appId: 'demo-app',
319
+ files,
320
+ });
321
+ ```
322
+
323
+ ### Step 7: Start the render loop
324
+
325
+ ```js
326
+ view.startRendering();
327
+ ```
328
+
329
+ ### Step 8: Clean up when the page is destroyed
330
+
331
+ ```js
332
+ window.addEventListener('beforeunload', () => {
333
+ view.destroy();
334
+ });
335
+ ```
336
+
337
+ ### Full Plain JavaScript Example
338
+
339
+ ```js
340
+ import { createInkView } from '@yodaos-pkg/ink';
341
+
342
+ const files = {
343
+ 'app.json': JSON.stringify({
344
+ pages: ['pages/index/index'],
345
+ }),
346
+ 'pages/index/index.js': `
347
+ export default {
348
+ data: {
349
+ title: 'Hello Ink'
350
+ }
351
+ };
352
+ `,
353
+ };
354
+
355
+ const canvas = document.getElementById('ink-root');
356
+
357
+ const view = await createInkView({
358
+ width: canvas.clientWidth,
359
+ height: canvas.clientHeight,
360
+ scaleFactor: window.devicePixelRatio,
361
+ canvas,
362
+ });
363
+
364
+ view.bindDomEvents();
365
+ view.openBundle({
366
+ appId: 'demo-app',
367
+ files,
368
+ });
369
+ view.startRendering();
370
+
371
+ window.addEventListener('beforeunload', () => {
372
+ view.destroy();
373
+ });
374
+ ```
375
+
376
+ ## Opening an App Through VFS
377
+
378
+ If you do not want to embed the full bundle in the browser and instead prefer
379
+ to fetch resources from the server, use `openFromVfs()`.
380
+
381
+ ### Step 1: Prepare an accessible VFS service
382
+
383
+ For example, the server may expose:
384
+
385
+ - `https://example.com/ink-vfs/apps/demo-app/manifest`
386
+ - `https://example.com/ink-vfs/apps/demo-app/files/...`
387
+
388
+ ### Step 2: Create the view
389
+
390
+ ```js
391
+ import { createInkView } from '@yodaos-pkg/ink';
392
+
393
+ const canvas = document.getElementById('ink-root');
394
+
395
+ const view = await createInkView({
396
+ width: canvas.clientWidth,
397
+ height: canvas.clientHeight,
398
+ scaleFactor: window.devicePixelRatio,
399
+ canvas,
400
+ });
401
+ ```
402
+
403
+ ### Step 3: Open the remote app
404
+
405
+ ```js
406
+ view.bindDomEvents();
407
+
408
+ await view.openFromVfs({
409
+ appId: 'demo-app',
410
+ baseUrl: 'https://example.com/ink-vfs',
411
+ });
412
+
413
+ view.startRendering();
414
+ ```
415
+
416
+ ### Step 4: Release resources when done
417
+
418
+ ```js
419
+ view.destroy();
420
+ ```
421
+
422
+ ## Vue.js Integration
423
+
424
+ The recommended approach is to wrap Ink inside a Vue component and manage it
425
+ through `onMounted` and `onBeforeUnmount`.
426
+
427
+ ### Step 1: Install the dependency
428
+
429
+ ```bash
430
+ npm install @yodaos-pkg/ink
431
+ ```
432
+
433
+ ### Step 2: Create a Vue component
434
+
435
+ Below is a minimal `InkCanvas.vue` example.
436
+
437
+ ### Step 3: Initialize in `onMounted`
438
+
439
+ - Get the `canvas`
440
+ - Create the `InkView`
441
+ - Bind DOM events
442
+ - Open the bundle
443
+ - Start rendering
444
+
445
+ ### Step 4: Clean up in `onBeforeUnmount`
446
+
447
+ - Call `view.destroy()`
448
+
449
+ ### Full Vue Example
450
+
451
+ ```vue
452
+ <script setup lang="ts">
453
+ import { onBeforeUnmount, onMounted, ref } from 'vue';
454
+ import { createInkView } from '@yodaos-pkg/ink';
455
+
456
+ const props = defineProps<{
457
+ appId: string;
458
+ files: Record<string, string | Uint8Array>;
459
+ }>();
460
+
461
+ const canvasRef = ref<HTMLCanvasElement | null>(null);
462
+ let cleanup = () => {};
463
+
464
+ onMounted(async () => {
465
+ const canvas = canvasRef.value;
466
+ if (!canvas) {
467
+ return;
468
+ }
469
+
470
+ const view = await createInkView({
471
+ width: canvas.clientWidth,
472
+ height: canvas.clientHeight,
473
+ scaleFactor: window.devicePixelRatio,
474
+ canvas,
475
+ });
476
+
477
+ view.bindDomEvents();
478
+ view.openBundle({
479
+ appId: props.appId,
480
+ files: props.files,
481
+ });
482
+ view.startRendering();
483
+
484
+ cleanup = () => view.destroy();
485
+ });
486
+
487
+ onBeforeUnmount(() => {
488
+ cleanup();
489
+ });
490
+ </script>
491
+
492
+ <template>
493
+ <canvas ref="canvasRef" style="width: 375px; height: 667px;" />
494
+ </template>
495
+ ```
496
+
497
+ ### Vue Integration Tips
498
+
499
+ - Treat `files` and `appId` as component inputs
500
+ - If the bundle changes, destroy the old view before creating a new one
501
+ - If the component size changes, call `view.resize()` from the host layer
502
+
503
+ ## React.js Integration
504
+
505
+ The recommended approach is to wrap Ink in a React component and manage its
506
+ lifecycle inside `useEffect`.
507
+
508
+ ### Step 1: Install the dependency
509
+
510
+ ```bash
511
+ npm install @yodaos-pkg/ink
512
+ ```
513
+
514
+ ### Step 2: Create a React component
515
+
516
+ The component keeps a `canvas ref` and creates or destroys the Ink view inside
517
+ `useEffect`.
518
+
519
+ ### Step 3: Initialize inside `useEffect`
520
+
521
+ - Get the `canvas`
522
+ - Create the `InkView`
523
+ - Bind DOM events
524
+ - Open the bundle
525
+ - Start rendering
526
+
527
+ ### Step 4: Destroy it in the effect cleanup
528
+
529
+ - Call `view.destroy()`
530
+
531
+ ### Full React Example
532
+
533
+ ```tsx
534
+ import { useEffect, useRef } from 'react';
535
+ import { createInkView } from '@yodaos-pkg/ink';
536
+
537
+ type InkCanvasProps = {
538
+ appId: string;
539
+ files: Record<string, string | Uint8Array>;
540
+ };
541
+
542
+ export function InkCanvas({ appId, files }: InkCanvasProps) {
543
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
544
+
545
+ useEffect(() => {
546
+ let disposed = false;
547
+ let cleanup = () => {};
548
+
549
+ async function mount() {
550
+ const canvas = canvasRef.current;
551
+ if (!canvas) {
552
+ return;
553
+ }
554
+
555
+ const view = await createInkView({
556
+ width: canvas.clientWidth,
557
+ height: canvas.clientHeight,
558
+ scaleFactor: window.devicePixelRatio,
559
+ canvas,
560
+ });
561
+
562
+ view.bindDomEvents();
563
+ view.openBundle({
564
+ appId,
565
+ files,
566
+ });
567
+ view.startRendering();
568
+
569
+ cleanup = () => view.destroy();
570
+ if (disposed) {
571
+ cleanup();
572
+ }
573
+ }
574
+
575
+ mount();
576
+
577
+ return () => {
578
+ disposed = true;
579
+ cleanup();
580
+ };
581
+ }, [appId, files]);
582
+
583
+ return <canvas ref={canvasRef} style={{ width: 375, height: 667 }} />;
584
+ }
585
+ ```
586
+
587
+ ### React Integration Tips
588
+
589
+ - Keep `files` as a stable reference when possible, so the view is not rebuilt
590
+ on every render
591
+ - If you need to switch apps, reinitialize when `appId` or a wrapping `key`
592
+ changes
593
+ - If the host size changes significantly, use `ResizeObserver` and call
594
+ `view.resize()`
595
+
596
+ ## Frequently Asked Questions
597
+
598
+ ### 1. The page renders, but input does not work
599
+
600
+ The usual cause is missing this step:
601
+
602
+ ```js
603
+ view.bindDomEvents();
604
+ ```
605
+
606
+ ### 2. Resources are still retained after the page is destroyed
607
+
608
+ Make sure you call this when the component unmounts or the page exits:
609
+
610
+ ```js
611
+ view.destroy();
612
+ ```
613
+
614
+ ### 3. Should bundle files be strings or binary?
615
+
616
+ Both are supported:
617
+
618
+ - `string`
619
+ - `Uint8Array`
620
+ - `ArrayBuffer`
621
+
622
+ ### 4. When should I use `openBundle()` vs `openFromVfs()`?
623
+
624
+ - If you already have the full resource contents, use `openBundle()`
625
+ - If you only know the `appId` and a server endpoint, use `openFromVfs()`
626
+
627
+ ## Local Development
628
+
629
+ ### Build
630
+
631
+ ```bash
632
+ npm run build
633
+ ```
634
+
635
+ ### Test
636
+
637
+ ```bash
638
+ npm test
639
+ ```
640
+
641
+ ## Package Shape
642
+
643
+ - `dist/index.js`
644
+ - SDK entry point
645
+ - `dist/pkg/*`
646
+ - wasm-bindgen output
647
+ - `dist/env.js`
648
+ - runtime import shim