@symbiote-native/solid 3.0.2 → 3.0.4

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 CHANGED
@@ -18,16 +18,29 @@ Metro, no custom Babel transformer path.
18
18
 
19
19
  ## Install
20
20
 
21
+ ```bash
22
+ npx @symbiote-native/cli new my-app --framework solid
23
+ ```
24
+
25
+ One command, nothing to wire by hand: scaffolds the Babel preset, Metro config, and
26
+ `jsxImportSource` below, plus `@symbiote-native/solid`/`react-native`/`solid-js` as your app's own
27
+ dependencies.
28
+
29
+ <details>
30
+ <summary>Manual install (no generator — an existing app, or you want to wire it yourself)</summary>
31
+
21
32
  ```bash
22
33
  npm install @symbiote-native/solid react-native solid-js
23
34
  ```
24
35
 
25
36
  `react-native` and `solid-js` stay your app's own top-level dependencies — this package only
26
- replaces the JS renderer that drives them. `npx @symbiote-native/cli new --framework solid` wires
27
- the Babel preset, Metro config, and `jsxImportSource` below for a new app; wiring them into an
28
- existing one still follows [`examples/solid`](../../examples/solid) rather than a generator.
37
+ replaces the JS renderer that drives them. Follow [`examples/solid`](../../examples/solid) for the
38
+ Babel preset, Metro config, and `jsxImportSource` below; there is no wiring script for an existing
39
+ app.
40
+
41
+ </details>
29
42
 
30
- Targets **Solid 1.9**, deliberately not 2.0 (RC as of 2026-08) — 2.0 moves the package to
43
+ Targets **Solid 1.9**, deliberately not 2.0 (still RC) — 2.0 moves the package to
31
44
  `@solidjs/universal`, changes `RendererOptions`, and changes the compiled-output shape. Moving to
32
45
  it is a rewrite of `src/renderer.ts`, not a range bump.
33
46
 
@@ -36,29 +49,49 @@ it is a rewrite of `src/renderer.ts`, not a range bump.
36
49
  ## Use it
37
50
 
38
51
  The app is ordinary Solid — the native primitives are plain lowercase intrinsic tags, no import
39
- needed:
52
+ needed. Styling is a CSS class against a plain `.css` file — the convention every example app
53
+ here follows:
40
54
 
41
55
  ```jsx
42
56
  import { createSignal } from 'solid-js';
57
+ import './App.css';
43
58
 
44
59
  export default function App() {
45
60
  const [count, setCount] = createSignal(0);
46
61
  return (
47
- <view style={{ padding: 24 }}>
62
+ <safe-area-view class="screen">
48
63
  <text>Taps: {count()}</text>
49
64
  <pressable onPress={() => setCount(c => c + 1)}>
50
65
  <text>Tap me</text>
51
66
  </pressable>
52
- </view>
67
+ </safe-area-view>
53
68
  );
54
69
  }
55
70
  ```
56
71
 
72
+ ```css
73
+ /* App.css */
74
+ .screen {
75
+ flex: 1;
76
+ align-items: center;
77
+ justify-content: center;
78
+ padding: 24px;
79
+ }
80
+ ```
81
+
82
+ <details>
83
+ <summary>Native entry point (index.js) — already scaffolded by <code>npx @symbiote-native/cli new --framework solid</code></summary>
84
+
57
85
  The zero-config entry wires the RN-backed host seams and registers the app in one call — this is
58
86
  what [`examples/solid`](../../examples/solid) actually uses:
59
87
 
60
88
  ```js
61
89
  // index.js
90
+
91
+ // Registers host behaviors (Image, Pressable, Switch, ...) that /bootstrap alone doesn't
92
+ // reach; deleting this breaks them silently (Metro's production inlineRequires makes a
93
+ // side-effect-only barrel import go lazy, see register.ts).
94
+ import '@symbiote-native/solid';
62
95
  import { createApp } from '@symbiote-native/solid/bootstrap';
63
96
  import App from './App';
64
97
  import { name as appName } from './app.json';
@@ -81,6 +114,8 @@ AppRegistry.registerRunnable(appName, ({ rootTag }) => {
81
114
  });
82
115
  ```
83
116
 
117
+ </details>
118
+
84
119
  `babel.config.js` needs the adapter's preset LAST — Babel applies presets in reverse order, so
85
120
  listing it last runs it first, claiming the JSX before RN's own React-JSX transform can:
86
121
 
@@ -100,11 +135,11 @@ Nothing rewrites the source on the way there, so this adapter carries no Babel p
100
135
  ## `./renderer` is a compiler target, not a convenience export
101
136
 
102
137
  `babel-preset-solid` with `generate: 'universal'` rewrites JSX into direct calls imported from the
103
- `moduleName` it was given. So `src/renderer.ts` exports eleven specific names because generated code
138
+ `moduleName` it was given. So `src/renderer.ts` exports twelve specific names because generated code
104
139
  imports them; dropping one breaks bundling with a module-not-found on an import nobody wrote. The
105
- list (`createElement`, `createTextNode`, `insertNode`, `insert`, `setProp`, `use`, `effect`, `memo`,
106
- `createComponent`, `spread`, `mergeProps`) was verified by compiling representative JSX, not read off
107
- the docs.
140
+ list (`render`, `createElement`, `createTextNode`, `insertNode`, `insert`, `setProp`, `use`, `effect`,
141
+ `memo`, `createComponent`, `spread`, `mergeProps`) was verified by compiling representative JSX, not
142
+ read off the docs.
108
143
 
109
144
  ## Three things the universal runtime does that the seam has to answer correctly
110
145
 
@@ -14,13 +14,9 @@
14
14
  // and assembles the host elements. Lists have no Descriptor render fn — the cell content is the
15
15
  // user's own subtree (`symbiote-add-component` §0, category 2).
16
16
  //
17
- // SINCE 2026-09-11 THIS FILE NO LONGER BUILDS THE CONTENT NODE ITSELF. `registerScrollViewBehavior()`
18
- // makes the tag's own `buildStructure` do that (the same content node ScrollView's bare tag gets
19
- // anywhere else), and this file reads it back off `scroll.childHost` rather than constructing a
20
- // second one — building one here too would nest a content view inside the behavior's own
21
- // (`core/components/src/behaviors/scroll-view/shared.ts`'s header names this precondition). The
22
- // RefreshControl placement (a sibling on iOS, an inverting wrap on Android) moved the same way: it is
23
- // an ordinary child now, and the behavior's `claimedChildren` decides where it lands.
17
+ // THIS FILE NO LONGER BUILDS THE CONTENT NODE ITSELF: `registerScrollViewBehavior()`'s tag-owned
18
+ // `buildStructure` does that, read back off `scroll.childHost` rather than built a second time
19
+ // here. RefreshControl is an ordinary child now; `claimedChildren` decides where it lands.
24
20
  //
25
21
  // WHY THIS FILE STILL HAND-AUTHORS THE SCROLL TAG VIA `createElement` INSTEAD OF PLAIN JSX
26
22
  // (`<scroll-view>{listBody}</scroll-view>`), unlike a component with no dynamic children. Solid's
@@ -496,24 +492,12 @@ export function createVirtualizedList() {
496
492
  // A spread of a CALL compiles to mergeProps(() => …), so the separator's props stay live.
497
493
  return <Separator {...separatorPropsFor(index)}/>;
498
494
  }
499
- // RN renders a separator in the gap AFTER a cell, and only while there is a following rendered
500
- // cell — never after the window's last one.
501
- // RN renders a separator in the gap AFTER a cell, and only while there is a following rendered
502
- // cell — never after the window's last one, and never after the force-mounted sticky cell, which
503
- // is not adjacent to the window.
495
+ // RN renders a separator in the gap AFTER a cell, only while there is a following rendered
496
+ // cell — never after the window's last one, and never after the force-mounted sticky cell,
497
+ // which is not adjacent to the window.
504
498
  // RN gates the separator on the last index of the DATA, not of the WINDOW
505
- // (VirtualizedList.js:793 `const end = getItemCount(data) - 1`), and now that the separator
506
- // lives INSIDE the measuring wrapper that distinction is load-bearing: gating on the window
507
- // would make a cell's own measured height change as the window slides past it. Device-measured
508
- // 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
509
- // Nothing about the WINDOW may enter this predicate. The separator lives inside the measuring
510
- // wrapper, so whatever decides to render it decides the cell's own height — and a height that
511
- // depends on where the window happens to sit moves the content under the user every time the
512
- // window slides. Two window-dependent terms were removed after being measured on device
513
- // 2026-08-19, each as a run of cells shifting by exactly the divider's 1px: gating on the
514
- // window's `last` (RN gates on the data's, VirtualizedList.js:793), and excluding the
515
- // force-mounted sticky cell. RN excludes neither — its sticky header keeps its separator like
516
- // any other cell.
499
+ // (VirtualizedList.js:793): the separator lives INSIDE the measuring wrapper, so gating on the
500
+ // window would make a cell's own measured height change as the window slides past it.
517
501
  const hasSeparatorAfter = (index) => props.ItemSeparatorComponent !== undefined && index < metrics().count - 1;
518
502
  // Records a sticky header's measured y (RN's _headerLayoutYs) so the header AHEAD of it can
519
503
  // compute its collision point; without it every header would stick indefinitely.
@@ -554,13 +538,9 @@ export function createVirtualizedList() {
554
538
  index: index(),
555
539
  separators: makeSeparators(index()),
556
540
  })));
557
- // The separator rides INSIDE the measuring wrapper, where RN's own cell renderer puts it
558
- // (VirtualizedListCellRenderer.js:218-221). As a SIBLING it is an extra flex child, so the
559
- // chrome between two cells is gap + separator + gap while a spacer collapsing that region
560
- // replaces it with one gap — every cell below the leading spacer then lands short by
561
- // (separator + gap), and the content jumps by that much each time the window's first index
562
- // moves. Measured at exactly 17px on device 2026-08-19 (a 1px divider under a 16px container
563
- // gap); see .claude/rules/list-geometry-feedback-loop.md.
541
+ // The separator rides INSIDE the measuring wrapper (VirtualizedListCellRenderer.js:218-221).
542
+ // As a SIBLING it would be an extra flex child, landing every cell below the leading spacer
543
+ // short by (separator + gap) — see `list-geometry-feedback-loop.md`.
564
544
  const separator = (<Show when={hasSeparatorAfter(index())}>
565
545
  <view>{separatorElement(index())}</view>
566
546
  </Show>);
@@ -1,15 +1,9 @@
1
1
  // Sticky headers — the Solid lifecycle half of the JS layer RN implements in ScrollView.js /
2
2
  // ScrollViewStickyHeader.js.
3
3
  //
4
- // PRIVATE TO VIRTUALIZED-LIST since 2026-09-11. It used to live under `../scroll-view` and back
5
- // that component's `stickyHeaderIndices` too; the tag `<scroll-view>` now honors that prop through
6
- // the engine's own `sticky-header` intrinsic (`core/components/src/behaviors/scroll-view/sticky.ts`)
7
- // with no adapter code at all — see that primitive's props header. VirtualizedList still needs its
8
- // OWN implementation because it hand-builds its scroll host (`shared.tsx`'s own header explains
9
- // why) rather than emitting the tag, so it wraps a flagged CELL itself instead of leaning on the
10
- // engine's per-child walk. `wrapStickyHeaders`/`IStickyWrapInputs` — the generic child-array wrapper
11
- // ScrollView used and this file never called — were dropped with the move; `virtualized-list/
12
- // shared.tsx` wraps the cell it already owns directly.
4
+ // PRIVATE TO VIRTUALIZED-LIST: `<scroll-view>` honors `stickyHeaderIndices` via the engine's own
5
+ // `sticky-header` intrinsic with no adapter code. VirtualizedList still needs its OWN
6
+ // implementation because it hand-builds its scroll host, so `shared.tsx` wraps the cell directly.
13
7
  //
14
8
  // RN does stickiness PURELY IN JS: ScrollView wraps each flagged child in a ScrollViewStickyHeader
15
9
  // fed by ONE scroll AnimatedValue, and the native scroll view ignores `stickyHeaderIndices`
package/build/renderer.js CHANGED
@@ -60,17 +60,11 @@ export function replaceText(textNode, value) {
60
60
  setEngineText(textNode, asText(value));
61
61
  requestCommit();
62
62
  }
63
- // solid-js/universal hands createTextNode/replaceText the JSX child value UNCONVERTED, not a
64
- // stringified one: normalizeIncomingArray() pushes `createTextNode(item)` straight from the
65
- // children array, so `<Text>{list().length} tiles</Text>` arrives here as the NUMBER 3. On the DOM
66
- // that is invisible — document.createTextNode coerces — which is why upstream never had to, and why
67
- // React's and Vue's adapters (whose frameworks stringify first) never saw it either. Here the value
68
- // lands on RCTRawText's `text`, which Fabric parses as a std::string: a number fails the
69
- // conversion, convertRawProp logs and falls back to the DEFAULT empty string, and an empty first
70
- // fragment then ABORTS the app inside BaseTextShadowNode::buildAttributedString — a native SIGABRT
71
- // with nothing in the JS stack to point at. Diagnosed on the iOS simulator 2026-08-19. The
72
- // parameter is typed `string` by RendererOptions, so the guard reads as redundant; it is not, and
73
- // `unknown` is what makes that honest rather than a cast.
63
+ // solid-js/universal hands createTextNode/replaceText the JSX child value UNCONVERTED —
64
+ // `<Text>{list().length} tiles</Text>` arrives here as the NUMBER 3. RCTRawText's `text` parses
65
+ // as a std::string: a number falls back to empty, aborting inside buildAttributedString.
66
+ // Typed `unknown` rather than `string` (which RendererOptions declares) so the runtime guard
67
+ // below isn't erased as dead code.
74
68
  function asText(value) {
75
69
  return typeof value === 'string' ? value : String(value);
76
70
  }
@@ -102,20 +96,11 @@ export function removeNode(parent, node) {
102
96
  removeEngineChild(parent, node);
103
97
  requestCommit();
104
98
  }
105
- // RN's two Text defaults left this renderer on 2026-09-18, and the per-key FOLD went with them. It
106
- // was the last and subtlest of the three shapes this file tried: a create-time SEED (gone a month
107
- // earlier, 6 000 wasted crossings per 1 000-row create), then a substitute-on-`undefined`, then a
108
- // fold per key — because `?? 'tail'` has to catch a null too, and substituting only on `undefined`
109
- // meant `<text ellipsizeMode={null}>` committed null, device-only and silent.
110
- //
111
- // All three were answering a question the layer below now answers for everyone: the rule reads the
112
- // AUTHORED bag at payload time (`foldTextDefaults`, `SymbioteFabricProps.cpp`), where a null, an
113
- // explicit `undefined` and an absent prop are all simply "not a value the author chose". The
114
- // null case that cost this file two revisions is `ellipsize->isNull()` there, in one place, for
115
- // every adapter.
116
- // The `id` -> `nativeID` fold and its `aliasedNodes` WeakSet left this file on 2026-09-18. The
117
- // memory was the right shape and the wrong LAYER: `routeProp` carries it now, so all five adapters
118
- // resolve the precedence identically instead of three of them doing it three ways.
99
+ // RN's two Text defaults are answered below the adapter now: the rule reads the AUTHORED bag at
100
+ // payload time (`foldTextDefaults`, `SymbioteFabricProps.cpp`), where a null, an explicit
101
+ // `undefined` and an absent prop are all "not a value the author chose" — one rule, every adapter.
102
+ // The `id` -> `nativeID` fold lives in `routeProp` now, so all five adapters resolve the
103
+ // precedence identically instead of each doing it their own way.
119
104
  // `multiline` selects between TWO Fabric views, so the TAG decides and no prop write moves a node
120
105
  // between them. An author writing `<text-input multiline>` instead of `<text-input-multiline>` gets
121
106
  // the single-line view with a prop no ViewConfig on it declares — two silent divergences.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/solid",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "SymbioteNative's Solid adapter — a solid-js/universal createRenderer driving real native iOS/Android views through the same engine as every other adapter.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -62,19 +62,19 @@
62
62
  "dependencies": {
63
63
  "babel-preset-solid": "^1.9.12",
64
64
  "typescript": "~6.0.0",
65
- "@symbiote-native/components": "3.1.0",
66
- "@symbiote-native/css-parser": "0.5.0"
65
+ "@symbiote-native/components": "3.1.2",
66
+ "@symbiote-native/css-parser": "0.5.2"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "solid-js": ">=1.9.0",
70
70
  "react-native": ">=0.86",
71
- "@symbiote-native/engine": "^1.3.0"
71
+ "@symbiote-native/engine": "^1.3.1"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@babel/core": "^7.25.2",
75
75
  "solid-js": "^1.9.14",
76
- "@symbiote-native/engine": "1.3.0",
77
- "@symbiote-native/test-utils": "0.4.2"
76
+ "@symbiote-native/engine": "1.3.1",
77
+ "@symbiote-native/test-utils": "0.4.4"
78
78
  },
79
79
  "scripts": {
80
80
  "typecheck": "tsc --build",