@ccheever/exact-renderer 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.
Files changed (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. package/src/web-primitives.ts +1654 -0
package/src/vue.ts ADDED
@@ -0,0 +1,535 @@
1
+ /**
2
+ * Exact Vue Renderer
3
+ *
4
+ * This module provides the Vue adapter for the Exact runtime.
5
+ * It uses @vue/runtime-core's createRenderer to map Vue's rendering
6
+ * operations to Exact's host operations.
7
+ *
8
+ * Usage:
9
+ * ```ts
10
+ * import { createApp } from './renderer/vue';
11
+ * import App from './vue-app';
12
+ *
13
+ * createApp(App).mount();
14
+ * ```
15
+ *
16
+ * Note: Vue components should use render functions (h) rather than
17
+ * template syntax for universal rendering.
18
+ */
19
+
20
+ import { createRenderer, type RendererOptions, nextTick } from '@vue/runtime-core';
21
+
22
+ import {
23
+ __setActiveWindowRenderer,
24
+ subscribeToRootWindowState,
25
+ } from '@exact/core';
26
+ import {
27
+ // Types
28
+ type ElementNode,
29
+ type TextNode,
30
+ type RootNode,
31
+
32
+ // Instance creation
33
+ createInstance,
34
+ createTextInstance,
35
+ createRoot,
36
+
37
+ // Tree operations
38
+ appendChild,
39
+ insertBefore,
40
+ removeChild,
41
+
42
+ // Updates
43
+ updateInstanceProps,
44
+ updateTextContent,
45
+ commitBatch,
46
+ destroyRoot,
47
+ syncRootWindowState,
48
+
49
+ // Node operations
50
+ NodeKind,
51
+
52
+ // Handler registry
53
+ _clearHandlers,
54
+ } from './host-ops.js';
55
+
56
+ // =============================================================================
57
+ // Types
58
+ // =============================================================================
59
+
60
+ type ExactNode = ElementNode | TextNode;
61
+ type ExactParentNode = ElementNode | RootNode;
62
+ type OptionalWindowRendererKind = 'solid' | 'vue';
63
+
64
+ interface WindowRendererRoot {
65
+ render(content: unknown): void;
66
+ unmount(): void;
67
+ }
68
+
69
+ interface RootState {
70
+ container: RootNode;
71
+ hasRenderedOnce: boolean;
72
+ commitScheduled: boolean;
73
+ claimed: boolean;
74
+ unsubscribeWindowState: (() => void) | null;
75
+ }
76
+
77
+ const rootStates = new Map<number, RootState>();
78
+
79
+ function registerOptionalWindowRenderer(
80
+ renderer: OptionalWindowRendererKind,
81
+ createRoot: (rootId: number) => WindowRendererRoot,
82
+ ): void {
83
+ const globalScope = globalThis as typeof globalThis & {
84
+ __exactOptionalWindowRenderers?:
85
+ Partial<Record<OptionalWindowRendererKind, (rootId: number) => WindowRendererRoot>>;
86
+ };
87
+ const registry = globalScope.__exactOptionalWindowRenderers ??= {};
88
+ registry[renderer] = createRoot;
89
+ }
90
+
91
+ function getRootContainerForNode(node: ExactNode | RootNode | null | undefined): RootNode | null {
92
+ let current = node;
93
+ while (current) {
94
+ if (current.kind === NodeKind.Root) {
95
+ return current;
96
+ }
97
+ current = current.parent as ExactNode | RootNode | null;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function getRootStateForNode(node: ExactNode | RootNode | null | undefined): RootState | null {
103
+ const container = getRootContainerForNode(node);
104
+ return container
105
+ ? rootStates.get(container.rootId) ?? null
106
+ : null;
107
+ }
108
+
109
+ function getOrCreateRootState(rootId: number): RootState {
110
+ let rootState = rootStates.get(rootId);
111
+ if (!rootState) {
112
+ rootState = {
113
+ container: createRoot(rootId, 'vue'),
114
+ hasRenderedOnce: false,
115
+ commitScheduled: false,
116
+ claimed: false,
117
+ unsubscribeWindowState:
118
+ typeof subscribeToRootWindowState === 'function'
119
+ ? subscribeToRootWindowState(rootId, () => {
120
+ if (rootState) {
121
+ syncRootWindowState(rootState.container);
122
+ }
123
+ })
124
+ : null,
125
+ };
126
+ rootStates.set(rootId, rootState);
127
+ }
128
+ return rootState;
129
+ }
130
+
131
+ function scheduleCommitForRoot(rootState: RootState | null): void {
132
+ if (!rootState || rootState.commitScheduled) {
133
+ return;
134
+ }
135
+
136
+ rootState.commitScheduled = true;
137
+ queueMicrotask(() => {
138
+ rootState.commitScheduled = false;
139
+ commitBatch(rootState.container);
140
+ });
141
+ }
142
+
143
+ function scheduleCommitForNode(node: ExactNode | RootNode | null | undefined): void {
144
+ scheduleCommitForRoot(getRootStateForNode(node));
145
+ }
146
+
147
+ // =============================================================================
148
+ // Vue Custom Renderer Options
149
+ // =============================================================================
150
+
151
+ /**
152
+ * Vue renderer options that map Vue's rendering operations
153
+ * to Exact's host operations.
154
+ */
155
+ const rendererOptions: RendererOptions<ExactNode, ExactParentNode> = {
156
+ /**
157
+ * Create a new element node.
158
+ */
159
+ createElement(type: string): ElementNode {
160
+ return createInstance(type, {});
161
+ },
162
+
163
+ /**
164
+ * Create a new text node.
165
+ */
166
+ createText(text: string): TextNode {
167
+ return createTextInstance(text);
168
+ },
169
+
170
+ /**
171
+ * Create a comment node (not used in Exact, return empty text).
172
+ */
173
+ createComment(text: string): TextNode {
174
+ // Comments aren't rendered in Exact, but Vue requires this
175
+ return createTextInstance('');
176
+ },
177
+
178
+ /**
179
+ * Set the text content of a text node.
180
+ */
181
+ setText(node: TextNode, text: string): void {
182
+ if (node.kind === NodeKind.Text) {
183
+ updateTextContent(node, text);
184
+ scheduleCommitForNode(node);
185
+ }
186
+ },
187
+
188
+ /**
189
+ * Set text content of an element.
190
+ * Vue calls this when an element has a single text child (e.g., h('text', {}, 'Hello')).
191
+ * We create/update a text node child to hold the content.
192
+ */
193
+ setElementText(node: ElementNode, text: string): void {
194
+
195
+ if (node.kind !== NodeKind.Element) {
196
+ return;
197
+ }
198
+
199
+ // Check if we already have a text node child we can update
200
+ const existingTextChild = node.children.find(
201
+ (child): child is TextNode => child.kind === NodeKind.Text
202
+ );
203
+
204
+ if (existingTextChild) {
205
+ // Update existing text node
206
+ updateTextContent(existingTextChild, text);
207
+ } else {
208
+ // Clear any existing children and add a text node
209
+ // Note: We need to properly remove existing children
210
+ while (node.children.length > 0) {
211
+ const child = node.children[0];
212
+ removeChild(node, child);
213
+ }
214
+
215
+ // Create and append new text node
216
+ const textNode = createTextInstance(text);
217
+ appendChild(node, textNode);
218
+ }
219
+
220
+ // Schedule commit
221
+ scheduleCommitForNode(node);
222
+ },
223
+
224
+ /**
225
+ * Insert a child into a parent.
226
+ */
227
+ insert(child: ExactNode, parent: ExactParentNode, anchor?: ExactNode | null): void {
228
+ if (anchor) {
229
+ insertBefore(parent, child, anchor);
230
+ } else {
231
+ appendChild(parent, child);
232
+ }
233
+ scheduleCommitForNode(parent);
234
+ },
235
+
236
+ /**
237
+ * Remove a child from its parent.
238
+ */
239
+ remove(child: ExactNode): void {
240
+ const parent = child.parent;
241
+ if (parent && (parent.kind === NodeKind.Element || parent.kind === NodeKind.Root)) {
242
+ const parentNode = parent as ExactParentNode;
243
+ removeChild(parentNode, child);
244
+ scheduleCommitForNode(parentNode);
245
+ }
246
+ },
247
+
248
+ /**
249
+ * Get the parent of a node.
250
+ */
251
+ parentNode(node: ExactNode): ExactParentNode | null {
252
+ return (node.parent as ExactParentNode) ?? null;
253
+ },
254
+
255
+ /**
256
+ * Get the next sibling of a node.
257
+ */
258
+ nextSibling(node: ExactNode): ExactNode | null {
259
+ const parent = node.parent;
260
+ if (!parent || (parent.kind !== NodeKind.Element && parent.kind !== NodeKind.Root)) {
261
+ return null;
262
+ }
263
+
264
+ const parentElement = parent as ExactParentNode;
265
+ const index = parentElement.children.indexOf(node as ElementNode);
266
+ if (index >= 0 && index < parentElement.children.length - 1) {
267
+ return parentElement.children[index + 1] as ExactNode;
268
+ }
269
+ return null;
270
+ },
271
+
272
+ /**
273
+ * Patch element props.
274
+ */
275
+ patchProp(
276
+ el: ElementNode,
277
+ key: string,
278
+ prevValue: unknown,
279
+ nextValue: unknown
280
+ ): void {
281
+ if (el.kind !== NodeKind.Element) {
282
+ return;
283
+ }
284
+
285
+ // Build new props object with the updated property
286
+ const newProps = { ...el.originalProps, [key]: nextValue };
287
+
288
+ // Use the shared updateInstanceProps
289
+ updateInstanceProps(el, el.originalProps || {}, newProps);
290
+
291
+ scheduleCommitForNode(el);
292
+ },
293
+
294
+ /**
295
+ * Clone a node (required but not commonly used).
296
+ */
297
+ cloneNode(node: ElementNode): ElementNode {
298
+ // Create a new instance with the same type and props
299
+ return createInstance(node.originalTag, node.originalProps || {});
300
+ },
301
+
302
+ /**
303
+ * Insert static content (for v-html, not used in Exact).
304
+ */
305
+ insertStaticContent(): [ExactNode, ExactNode] {
306
+ // Return dummy nodes - static content not supported
307
+ const node = createTextInstance('');
308
+ return [node, node];
309
+ },
310
+ };
311
+
312
+ // =============================================================================
313
+ // Create the Vue Renderer
314
+ // =============================================================================
315
+
316
+ const { render: vueRender, createApp: vueCreateApp } = createRenderer(rendererOptions);
317
+
318
+ // =============================================================================
319
+ // Public API
320
+ // =============================================================================
321
+
322
+ /**
323
+ * Create a Vue application instance for Exact.
324
+ *
325
+ * @param rootComponent - The root Vue component
326
+ * @param rootProps - Optional props for the root component
327
+ * @returns A Vue app instance with a custom mount method
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * import { createApp, ref, h } from './renderer/vue';
332
+ *
333
+ * const App = {
334
+ * setup() {
335
+ * const count = ref(0);
336
+ * return () => h('div', { style: { flex: 1 } }, [
337
+ * h('text', {}, `Count: ${count.value}`)
338
+ * ]);
339
+ * }
340
+ * };
341
+ *
342
+ * createApp(App).mount();
343
+ * ```
344
+ */
345
+ export function createApp(rootComponent: any, rootProps?: Record<string, unknown>) {
346
+ __setActiveWindowRenderer?.('vue');
347
+ const rootState = getOrCreateRootState(0);
348
+
349
+ const app = vueCreateApp(rootComponent, rootProps);
350
+
351
+ // Override mount to use our root container
352
+ const originalMount = app.mount;
353
+ app.mount = () => {
354
+ const proxy = originalMount(rootState.container as unknown as any);
355
+
356
+ // Commit the initial batch to native
357
+ commitBatch(rootState.container);
358
+ rootState.hasRenderedOnce = true;
359
+
360
+ return proxy;
361
+ };
362
+
363
+ return app;
364
+ }
365
+
366
+ /**
367
+ * Render a Vue vnode directly to the Exact runtime.
368
+ *
369
+ * Note: Vue's component-level reactivity (watchEffect, component render effects)
370
+ * doesn't work properly with custom renderers. Use this with a raw `effect` from
371
+ * @vue/reactivity to trigger re-renders when reactive state changes.
372
+ *
373
+ * @param vnode - The Vue vnode to render
374
+ * @returns A cleanup function
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * import { effect } from '@vue/reactivity';
379
+ * import { render, ref, h } from './renderer/vue';
380
+ *
381
+ * const count = ref(0);
382
+ * effect(() => {
383
+ * render(h('text', {}, `Count: ${count.value}`));
384
+ * });
385
+ * ```
386
+ */
387
+ export function render(vnode: any): () => void {
388
+ __setActiveWindowRenderer?.('vue');
389
+ const rootState = getOrCreateRootState(0);
390
+
391
+ vueRender(vnode, rootState.container as unknown as any);
392
+
393
+ // First render: commit immediately to bind events.
394
+ // Subsequent renders: scheduleCommit in patchProp/setElementText handles commits.
395
+ if (!rootState.hasRenderedOnce) {
396
+ commitBatch(rootState.container);
397
+ rootState.hasRenderedOnce = true;
398
+ }
399
+
400
+ return () => {
401
+ vueRender(null, rootState.container as unknown as any);
402
+ };
403
+ }
404
+
405
+ export interface ExactRoot {
406
+ readonly rootId: number;
407
+ render(vnode: any): void;
408
+ unmount(): void;
409
+ }
410
+
411
+ export function createExactRoot(rootId: number): ExactRoot {
412
+ if (rootId === 0) {
413
+ throw new Error('createExactRoot: rootId must be > 0. Use render() for the default root.');
414
+ }
415
+
416
+ const rootState = getOrCreateRootState(rootId);
417
+ if (rootState.claimed) {
418
+ throw new Error(`createExactRoot: rootId ${rootId} is already in use.`);
419
+ }
420
+ rootState.claimed = true;
421
+
422
+ return {
423
+ rootId,
424
+
425
+ render(vnode: any): void {
426
+ __setActiveWindowRenderer?.('vue');
427
+ vueRender(vnode, rootState.container as unknown as any);
428
+ if (!rootState.hasRenderedOnce) {
429
+ commitBatch(rootState.container);
430
+ rootState.hasRenderedOnce = true;
431
+ }
432
+ },
433
+
434
+ unmount(): void {
435
+ vueRender(null, rootState.container as unknown as any);
436
+ rootStates.delete(rootId);
437
+ rootState.unsubscribeWindowState?.();
438
+ rootState.unsubscribeWindowState = null;
439
+ destroyRoot(rootId);
440
+ },
441
+ };
442
+ }
443
+
444
+ registerOptionalWindowRenderer(
445
+ 'vue',
446
+ createExactRoot as (rootId: number) => WindowRendererRoot,
447
+ );
448
+
449
+ /**
450
+ * Fully reset the renderer state.
451
+ */
452
+ export function reset(): void {
453
+ for (const [rootId, rootState] of rootStates) {
454
+ vueRender(null, rootState.container as unknown as any);
455
+ rootState.unsubscribeWindowState?.();
456
+ rootState.unsubscribeWindowState = null;
457
+ destroyRoot(rootId);
458
+ }
459
+ rootStates.clear();
460
+ _clearHandlers();
461
+ }
462
+
463
+ // =============================================================================
464
+ // Re-exports from Vue
465
+ // =============================================================================
466
+
467
+ // Composition API
468
+ export {
469
+ ref,
470
+ reactive,
471
+ readonly,
472
+ computed,
473
+ watch,
474
+ watchEffect,
475
+ watchPostEffect,
476
+ watchSyncEffect,
477
+
478
+ // Lifecycle
479
+ onMounted,
480
+ onUpdated,
481
+ onUnmounted,
482
+ onBeforeMount,
483
+ onBeforeUpdate,
484
+ onBeforeUnmount,
485
+
486
+ // Dependency Injection
487
+ provide,
488
+ inject,
489
+
490
+ // Utilities
491
+ unref,
492
+ toRef,
493
+ toRefs,
494
+ isRef,
495
+ isReactive,
496
+ isReadonly,
497
+ isProxy,
498
+ shallowRef,
499
+ triggerRef,
500
+ shallowReactive,
501
+ shallowReadonly,
502
+ toRaw,
503
+ markRaw,
504
+ effectScope,
505
+ getCurrentScope,
506
+ onScopeDispose,
507
+ nextTick,
508
+
509
+ // Render helpers
510
+ h,
511
+ createVNode,
512
+ mergeProps,
513
+ cloneVNode,
514
+ isVNode,
515
+ resolveComponent,
516
+ resolveDirective,
517
+ withDirectives,
518
+
519
+ // Async
520
+ defineAsyncComponent,
521
+
522
+ // Special components
523
+ Fragment,
524
+ Teleport,
525
+ Suspense,
526
+ KeepAlive,
527
+
528
+ // Types
529
+ type Ref,
530
+ type ComputedRef,
531
+ type ShallowRef,
532
+ type UnwrapRef,
533
+ type Component,
534
+ type VNode,
535
+ } from '@vue/runtime-core';
@@ -0,0 +1,19 @@
1
+ // @system @ref LLP 0201 W1 / LLP 0202 H1 — production Contract web host CSR.
2
+
3
+ import {
4
+ installContractWebHost as installContractWebHostBase,
5
+ type ContractWebHostHandle,
6
+ type ContractWebHostOptions,
7
+ } from './dom-mirror.js';
8
+ import { installDomHydrationAdopterFactory } from './dom-hydration.js';
9
+
10
+ export type { ContractWebHostHandle, ContractWebHostOptions };
11
+
12
+ export function installContractWebHost(
13
+ options: ContractWebHostOptions = {},
14
+ ): ContractWebHostHandle | null {
15
+ if (options.hydrate) {
16
+ installDomHydrationAdopterFactory();
17
+ }
18
+ return installContractWebHostBase(options);
19
+ }