@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/solid.ts ADDED
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Exact Solid Renderer
3
+ *
4
+ * This module provides the Solid adapter for the Exact runtime.
5
+ * It uses solid-js/universal's createRenderer to map Solid's rendering
6
+ * operations to Exact's host operations.
7
+ *
8
+ * Usage:
9
+ * ```tsx
10
+ * import { render } from './renderer/solid';
11
+ * import App from './solid-app';
12
+ *
13
+ * render(() => <App />);
14
+ * ```
15
+ *
16
+ * Note: Solid JSX must be compiled with babel-preset-solid configured for
17
+ * universal rendering. See vite.config.solid.ts.
18
+ */
19
+
20
+ import './jsx-augmentations.js';
21
+
22
+ import { createRenderer } from 'solid-js/universal';
23
+ import { createSignal } from 'solid-js';
24
+ import type { JSX } from 'solid-js';
25
+
26
+ import {
27
+ __setActiveWindowRenderer,
28
+ subscribeToRootWindowState,
29
+ } from '@exact/core';
30
+ import {
31
+ // Types
32
+ type ElementNode,
33
+ type TextNode,
34
+ type RootNode,
35
+
36
+ // Instance creation
37
+ createInstance,
38
+ createTextInstance,
39
+ createRoot,
40
+
41
+ // Tree operations
42
+ appendChild,
43
+ insertBefore,
44
+ removeChild,
45
+
46
+ // Updates
47
+ updateInstanceProps,
48
+ updateTextContent,
49
+ commitBatch,
50
+ destroyRoot,
51
+ syncRootWindowState,
52
+
53
+ // Node operations
54
+ NodeKind,
55
+
56
+ // Handler registry
57
+ _clearHandlers,
58
+ } from './host-ops.js';
59
+
60
+ // =============================================================================
61
+ // Development Mode Flag
62
+ // =============================================================================
63
+
64
+ const __DEV__ = process.env.NODE_ENV !== 'production';
65
+
66
+ // =============================================================================
67
+ // Types
68
+ // =============================================================================
69
+
70
+ type ExactNode = ElementNode | TextNode;
71
+ type SolidNode = ExactNode | RootNode;
72
+ type RootParent = ElementNode | RootNode;
73
+ type OptionalWindowRendererKind = 'solid' | 'vue';
74
+
75
+ interface WindowRendererRoot {
76
+ render(content: unknown): void;
77
+ unmount(): void;
78
+ }
79
+
80
+ interface RootState {
81
+ container: RootNode;
82
+ dispose: (() => void) | null;
83
+ claimed: boolean;
84
+ code: (() => JSX.Element) | null;
85
+ windowVersion: (() => number) | null;
86
+ bumpWindowVersion: (() => void) | null;
87
+ unsubscribeWindowState: (() => void) | null;
88
+ }
89
+
90
+ function getRootContainerForNode(node: SolidNode | null | undefined): RootNode | null {
91
+ let current = node;
92
+ while (current) {
93
+ if (current.kind === NodeKind.Root) {
94
+ return current;
95
+ }
96
+ current = current.parent as SolidNode | null;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function commitNodeRoot(node: SolidNode | null | undefined): void {
102
+ const root = getRootContainerForNode(node);
103
+ if (root) {
104
+ commitBatch(root);
105
+ }
106
+ }
107
+
108
+ const rootStates = new Map<number, RootState>();
109
+
110
+ function registerOptionalWindowRenderer(
111
+ renderer: OptionalWindowRendererKind,
112
+ createRoot: (rootId: number) => WindowRendererRoot,
113
+ ): void {
114
+ const globalScope = globalThis as typeof globalThis & {
115
+ __exactOptionalWindowRenderers?:
116
+ Partial<Record<OptionalWindowRendererKind, (rootId: number) => WindowRendererRoot>>;
117
+ };
118
+ const registry = globalScope.__exactOptionalWindowRenderers ??= {};
119
+ registry[renderer] = createRoot;
120
+ }
121
+
122
+ function getOrCreateRootState(rootId: number): RootState {
123
+ let rootState = rootStates.get(rootId);
124
+ if (!rootState) {
125
+ const [windowVersion, setWindowVersion] = createSignal(0);
126
+ rootState = {
127
+ container: createRoot(rootId, 'solid'),
128
+ dispose: null,
129
+ claimed: false,
130
+ code: null,
131
+ windowVersion,
132
+ bumpWindowVersion: () => {
133
+ setWindowVersion((value) => value + 1);
134
+ },
135
+ unsubscribeWindowState: null,
136
+ };
137
+ rootState.unsubscribeWindowState =
138
+ typeof subscribeToRootWindowState === 'function'
139
+ ? subscribeToRootWindowState(rootId, () => {
140
+ if (rootState?.bumpWindowVersion) {
141
+ rootState.bumpWindowVersion();
142
+ }
143
+ syncRootWindowState(rootState.container);
144
+ })
145
+ : null;
146
+ rootStates.set(rootId, rootState);
147
+ }
148
+ return rootState;
149
+ }
150
+
151
+ // =============================================================================
152
+ // Solid Universal Renderer
153
+ // =============================================================================
154
+
155
+ /**
156
+ * Create the Solid universal renderer with Exact host operations.
157
+ *
158
+ * The createRenderer function from solid-js/universal returns rendering
159
+ * primitives that Solid's compiled JSX will use.
160
+ */
161
+ const renderer = createRenderer<SolidNode>({
162
+ /**
163
+ * Create a new element node.
164
+ */
165
+ createElement(type: string): ElementNode {
166
+ return createInstance(type, {});
167
+ },
168
+
169
+ /**
170
+ * Create a new text node.
171
+ */
172
+ createTextNode(value: string): TextNode {
173
+ return createTextInstance(value);
174
+ },
175
+
176
+ /**
177
+ * Update a text node's content.
178
+ */
179
+ replaceText(textNode: TextNode, value: string): void {
180
+ updateTextContent(textNode, value);
181
+ commitNodeRoot(textNode);
182
+ },
183
+
184
+ /**
185
+ * Check if a node is a text node.
186
+ */
187
+ isTextNode(node: SolidNode): node is TextNode {
188
+ return node.kind === NodeKind.Text;
189
+ },
190
+
191
+ /**
192
+ * Set a property on an element.
193
+ */
194
+ setProperty(node: SolidNode, name: string, value: unknown, prev?: unknown): void {
195
+ if (node.kind !== NodeKind.Element) {
196
+ return;
197
+ }
198
+
199
+ const element = node as ElementNode;
200
+
201
+ // Build new props object with the updated property
202
+ const newProps = { ...element.originalProps, [name]: value };
203
+
204
+ // Use the shared updateInstanceProps
205
+ updateInstanceProps(element, element.originalProps || {}, newProps);
206
+ commitNodeRoot(element);
207
+ },
208
+
209
+ /**
210
+ * Insert a node into a parent.
211
+ * If `anchor` is provided, insert before it; otherwise append.
212
+ */
213
+ insertNode(parent: SolidNode, node: SolidNode, anchor?: SolidNode | null): void {
214
+ if (
215
+ node.kind === NodeKind.Root ||
216
+ parent.kind !== NodeKind.Element && parent.kind !== NodeKind.Root
217
+ ) {
218
+ return;
219
+ }
220
+
221
+ const parentNode = parent as RootParent;
222
+ if (anchor) {
223
+ if (anchor.kind === NodeKind.Root) {
224
+ return;
225
+ }
226
+ insertBefore(parentNode, node, anchor);
227
+ } else {
228
+ appendChild(parentNode, node);
229
+ }
230
+ commitNodeRoot(parentNode);
231
+ },
232
+
233
+ /**
234
+ * Remove a node from its parent.
235
+ */
236
+ removeNode(parent: SolidNode, node: SolidNode): void {
237
+ if (
238
+ node.kind === NodeKind.Root ||
239
+ parent.kind !== NodeKind.Element && parent.kind !== NodeKind.Root
240
+ ) {
241
+ return;
242
+ }
243
+
244
+ const parentNode = parent as RootParent;
245
+ removeChild(parentNode, node);
246
+ commitNodeRoot(parentNode);
247
+ },
248
+
249
+ /**
250
+ * Get the parent node.
251
+ * Note: We track parent in our node structure.
252
+ */
253
+ getParentNode(node: SolidNode): SolidNode | undefined {
254
+ return node.parent as SolidNode | undefined;
255
+ },
256
+
257
+ /**
258
+ * Get the first child of a node.
259
+ */
260
+ getFirstChild(node: SolidNode): ExactNode | undefined {
261
+ if (node.kind === NodeKind.Element || node.kind === NodeKind.Root) {
262
+ const parentNode = node as RootParent;
263
+ return parentNode.children[0] as ExactNode | undefined;
264
+ }
265
+ return undefined;
266
+ },
267
+
268
+ /**
269
+ * Get the next sibling of a node.
270
+ */
271
+ getNextSibling(node: SolidNode): ExactNode | undefined {
272
+ const parent = node.parent;
273
+ if (!parent || (parent.kind !== NodeKind.Element && parent.kind !== NodeKind.Root)) {
274
+ return undefined;
275
+ }
276
+
277
+ const parentElement = parent as RootParent;
278
+ const index = parentElement.children.indexOf(node as ElementNode);
279
+ if (index >= 0 && index < parentElement.children.length - 1) {
280
+ return parentElement.children[index + 1] as ExactNode;
281
+ }
282
+ return undefined;
283
+ },
284
+ });
285
+
286
+ // Extract all the render primitives that Solid's JSX compiler expects
287
+ const {
288
+ render: solidRender,
289
+ effect,
290
+ memo,
291
+ createComponent,
292
+ createElement,
293
+ createTextNode,
294
+ insertNode,
295
+ insert,
296
+ spread,
297
+ setProp,
298
+ mergeProps,
299
+ } = renderer;
300
+
301
+ // =============================================================================
302
+ // Public API
303
+ // =============================================================================
304
+
305
+ /**
306
+ * Render a Solid component tree to the Exact runtime.
307
+ *
308
+ * @param code - A function returning the Solid component tree
309
+ * @returns A dispose function to unmount the tree
310
+ *
311
+ * @example
312
+ * ```tsx
313
+ * import { render } from './renderer/solid';
314
+ *
315
+ * function App() {
316
+ * const [count, setCount] = createSignal(0);
317
+ * return (
318
+ * <div style={{ flex: 1, backgroundColor: '#fff' }}>
319
+ * Count: {count()}
320
+ * </div>
321
+ * );
322
+ * }
323
+ *
324
+ * const dispose = render(() => <App />);
325
+ * ```
326
+ */
327
+ export function render(code: () => JSX.Element): () => void {
328
+ __setActiveWindowRenderer?.('solid');
329
+ const rootState = getOrCreateRootState(0);
330
+ rootState.code = code;
331
+
332
+ // Dispose previous render if any
333
+ if (rootState.dispose) {
334
+ rootState.dispose();
335
+ }
336
+
337
+ // Render the Solid component tree
338
+ rootState.dispose = solidRender(
339
+ (() => {
340
+ // The read forces Solid to revisit the full tree when root window state
341
+ // changes. That keeps hook-level/window-dependent computations fresh.
342
+ rootState.windowVersion?.();
343
+ return code();
344
+ }) as unknown as () => SolidNode,
345
+ rootState.container as unknown as ExactNode,
346
+ );
347
+
348
+ // Commit the initial batch to native
349
+ commitBatch(rootState.container);
350
+
351
+ return () => {
352
+ if (rootState.dispose) {
353
+ rootState.dispose();
354
+ rootState.dispose = null;
355
+ }
356
+ rootState.code = null;
357
+ };
358
+ }
359
+
360
+ export interface ExactRoot {
361
+ readonly rootId: number;
362
+ render(code: () => JSX.Element): void;
363
+ unmount(): void;
364
+ }
365
+
366
+ export function createExactRoot(rootId: number): ExactRoot {
367
+ if (rootId === 0) {
368
+ throw new Error('createExactRoot: rootId must be > 0. Use render() for the default root.');
369
+ }
370
+
371
+ const rootState = getOrCreateRootState(rootId);
372
+ if (rootState.claimed) {
373
+ throw new Error(`createExactRoot: rootId ${rootId} is already in use.`);
374
+ }
375
+ rootState.claimed = true;
376
+
377
+ return {
378
+ rootId,
379
+
380
+ render(code: () => JSX.Element): void {
381
+ __setActiveWindowRenderer?.('solid');
382
+ rootState.code = code;
383
+ if (rootState.dispose) {
384
+ rootState.dispose();
385
+ }
386
+ rootState.dispose = solidRender(
387
+ (() => {
388
+ rootState.windowVersion?.();
389
+ return code();
390
+ }) as unknown as () => SolidNode,
391
+ rootState.container as unknown as ExactNode,
392
+ );
393
+ commitBatch(rootState.container);
394
+ },
395
+
396
+ unmount(): void {
397
+ if (rootState.dispose) {
398
+ rootState.dispose();
399
+ rootState.dispose = null;
400
+ }
401
+ rootState.code = null;
402
+ rootState.unsubscribeWindowState?.();
403
+ rootState.unsubscribeWindowState = null;
404
+ rootState.claimed = false;
405
+ rootStates.delete(rootId);
406
+ destroyRoot(rootId);
407
+ },
408
+ };
409
+ }
410
+
411
+ registerOptionalWindowRenderer(
412
+ 'solid',
413
+ createExactRoot as (rootId: number) => WindowRendererRoot,
414
+ );
415
+
416
+ /**
417
+ * Unmount the current Solid tree.
418
+ */
419
+ export function unmount(): void {
420
+ const rootState = rootStates.get(0);
421
+ if (rootState?.dispose) {
422
+ rootState.dispose();
423
+ rootState.dispose = null;
424
+ }
425
+ if (rootState) {
426
+ rootState.code = null;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Fully reset the renderer state.
432
+ */
433
+ export function reset(): void {
434
+ for (const [rootId, rootState] of rootStates) {
435
+ if (rootState.dispose) {
436
+ rootState.dispose();
437
+ rootState.dispose = null;
438
+ }
439
+ rootState.code = null;
440
+ rootState.unsubscribeWindowState?.();
441
+ rootState.unsubscribeWindowState = null;
442
+ destroyRoot(rootId);
443
+ }
444
+ rootStates.clear();
445
+ _clearHandlers();
446
+ }
447
+
448
+ // =============================================================================
449
+ // Re-exports for Solid components
450
+ // =============================================================================
451
+
452
+ // Export Solid primitives for use in components and JSX compilation
453
+ export {
454
+ effect,
455
+ memo,
456
+ createComponent,
457
+ createElement,
458
+ createTextNode,
459
+ insertNode,
460
+ insert,
461
+ spread,
462
+ setProp,
463
+ mergeProps,
464
+ };
465
+
466
+ // Re-export solid-js core APIs
467
+ export {
468
+ createSignal,
469
+ createEffect,
470
+ createMemo,
471
+ createResource,
472
+ onMount,
473
+ onCleanup,
474
+ batch,
475
+ untrack,
476
+ on,
477
+ children,
478
+ Show,
479
+ For,
480
+ Index,
481
+ Switch,
482
+ Match,
483
+ ErrorBoundary,
484
+ Suspense,
485
+ } from 'solid-js';
486
+
487
+ export { Dynamic } from 'solid-js/web';
488
+
489
+ // Re-export component types
490
+ export type { Component, JSX } from 'solid-js';
@@ -0,0 +1 @@
1
+ export * from './color.ts';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Color Parsing
3
+ *
4
+ * Relocated to `@exact/core` per LLP 0157 F0 so framework-neutral packages
5
+ * (`@exact/facet-core`) can share one color implementation. This module
6
+ * re-exports the relocated functions for back-compat with renderer-relative
7
+ * imports.
8
+ */
9
+
10
+ export {
11
+ colorToHex,
12
+ colorToRgba,
13
+ isValidColor,
14
+ parseColor,
15
+ } from '@exact/core/style/color';
@@ -0,0 +1 @@
1
+ export * from './index.ts';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Style Normalization - Public Exports
3
+ *
4
+ * This module re-exports the style normalization utilities.
5
+ */
6
+
7
+ export {
8
+ parseColor,
9
+ isValidColor,
10
+ colorToHex,
11
+ colorToRgba,
12
+ } from './color.js';
13
+
14
+ export {
15
+ parseDimension,
16
+ parseFontWeight,
17
+ extractTransforms,
18
+ normalizeTransition,
19
+ normalizeStyle,
20
+ transitionsEqual,
21
+ stylesEqual,
22
+ } from './normalize.js';
@@ -0,0 +1 @@
1
+ export * from './normalize.ts';