@symbiote-native/svelte 3.0.2 → 3.0.3
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @symbiote-native/svelte
|
|
2
2
|
|
|
3
3
|
The **Svelte adapter** for [SymbioteNative](../../README.md) — render real native iOS/Android views
|
|
4
|
-
from Svelte 5, on the _same_ untouched core as React, Vue, and Angular, with React Native's own
|
|
4
|
+
from Svelte 5, on the _same_ untouched core as React, Vue, Solid, and Angular, with React Native's own
|
|
5
5
|
renderer never in the path. Svelte ships no official host-renderer API yet (`createRenderer` is
|
|
6
6
|
still an unmerged PR upstream), so instead of hooking a framework-blessed extension point this
|
|
7
7
|
adapter patches `globalThis`'s DOM classes: stock compiled Svelte client output believes it is
|
|
@@ -23,35 +23,84 @@ Svelte is the **sharpest test yet** that the core is genuinely framework-agnosti
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
+
```bash
|
|
27
|
+
npx @symbiote-native/cli new my-app --framework svelte
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
One command, nothing to wire by hand: every component here is a `.svelte` SFC with no TSX
|
|
31
|
+
alternative, so the Metro wiring below isn't optional — the generator sets it up, plus
|
|
32
|
+
`@symbiote-native/svelte`/`react-native`/`svelte` as your app's own dependencies.
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>Manual install (no generator — an existing app, or you want to wire it yourself)</summary>
|
|
36
|
+
|
|
26
37
|
```bash
|
|
27
38
|
npm install @symbiote-native/svelte react-native svelte
|
|
28
39
|
```
|
|
29
40
|
|
|
30
41
|
`react-native` and `svelte` stay your app's own top-level dependencies — this package only
|
|
31
|
-
replaces the JS renderer that drives them.
|
|
32
|
-
|
|
33
|
-
sets this up for a new app; wiring it into an existing one still follows
|
|
34
|
-
[`examples/svelte`](../../examples/svelte) rather than a generator:
|
|
42
|
+
replaces the JS renderer that drives them. Follow [`examples/svelte`](../../examples/svelte) for
|
|
43
|
+
both pieces of Metro wiring below; there is no wiring script for an existing app:
|
|
35
44
|
|
|
36
45
|
- `metro.config.js` — point `babelTransformerPath` at
|
|
37
46
|
`@symbiote-native/svelte/metro-svelte-transformer` (compiles `.svelte` on the way into the
|
|
38
47
|
bundle) and disable `inlineRequires` (see [the gotcha
|
|
39
48
|
below](#a-svelte-specific-gotcha--inlinerequires-vs-the-svelte-runtime)).
|
|
40
|
-
- `svelte.config.js` — registers the `forbidWebOnlyConstructs()
|
|
41
|
-
preprocessors, so `svelte-check` and the editor catch the same things
|
|
42
|
-
already guards against at build time
|
|
43
|
-
|
|
49
|
+
- `svelte.config.js` — registers the `forbidWebOnlyConstructs()`, `scopedStyles()`, and
|
|
50
|
+
`collapseTextWhitespace()` preprocessors, so `svelte-check` and the editor catch the same things
|
|
51
|
+
Metro's transformer already guards against at build time (the third one matters beyond
|
|
52
|
+
diagnostics: without it, a sentence wrapped across source lines for readability ships a literal
|
|
53
|
+
newline into the native text content — Svelte doesn't collapse it the way a browser or Vue's
|
|
54
|
+
compiler would). Also sets `compilerOptions: { fragments: 'tree', css: 'external' }` and a
|
|
55
|
+
`warningFilter` that silences `element_invalid_self_closing_tag` for `<pressable />`-style
|
|
56
|
+
self-closing primitives.
|
|
57
|
+
|
|
58
|
+
</details>
|
|
44
59
|
|
|
45
60
|
---
|
|
46
61
|
|
|
47
62
|
## Use it
|
|
48
63
|
|
|
64
|
+
The app is ordinary Svelte 5 — the native primitives are plain intrinsic tags, no import needed.
|
|
65
|
+
Styling is a CSS class against the component's own `<style>` block — the convention every example
|
|
66
|
+
app here follows. A tap→increment counter, using runes:
|
|
67
|
+
|
|
68
|
+
```svelte
|
|
69
|
+
<script lang="ts">
|
|
70
|
+
let count = $state(0);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<safe-area-view class="screen">
|
|
74
|
+
<text>Taps: {count}</text>
|
|
75
|
+
<pressable onPress={() => count++}>
|
|
76
|
+
<text>Tap me</text>
|
|
77
|
+
</pressable>
|
|
78
|
+
</safe-area-view>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.screen {
|
|
82
|
+
flex: 1;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
padding: 24px;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary>Native entry point (index.js) — already scaffolded by <code>npx @symbiote-native/cli new --framework svelte</code></summary>
|
|
92
|
+
|
|
49
93
|
The native entry reaches the _same_ seam as every other adapter. `createApp(App).mount(appName)`
|
|
50
94
|
wires the native-host seams and RN's own `AppRegistry`, then mounts via `@symbiote-native/engine` —
|
|
51
95
|
RN's own renderer is never in the path:
|
|
52
96
|
|
|
53
97
|
```js
|
|
54
98
|
// index.js
|
|
99
|
+
|
|
100
|
+
// Registers host behaviors (Image, Pressable, Switch, ...) that /bootstrap alone doesn't
|
|
101
|
+
// reach; deleting this breaks them silently (Metro's production inlineRequires makes a
|
|
102
|
+
// side-effect-only barrel import go lazy, see register.ts).
|
|
103
|
+
import '@symbiote-native/svelte';
|
|
55
104
|
import { createApp } from '@symbiote-native/svelte/bootstrap';
|
|
56
105
|
import App from './App.svelte';
|
|
57
106
|
import { name as appName } from './app.json';
|
|
@@ -59,21 +108,7 @@ import { name as appName } from './app.json';
|
|
|
59
108
|
createApp(App).mount(appName);
|
|
60
109
|
```
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
A tap→increment counter, using runes:
|
|
64
|
-
|
|
65
|
-
```svelte
|
|
66
|
-
<script lang="ts">
|
|
67
|
-
let count = $state(0);
|
|
68
|
-
</script>
|
|
69
|
-
|
|
70
|
-
<view style={{ padding: 24 }}>
|
|
71
|
-
<text>Taps: {count}</text>
|
|
72
|
-
<pressable onPress={() => count++}>
|
|
73
|
-
<text>Tap me</text>
|
|
74
|
-
</pressable>
|
|
75
|
-
</view>
|
|
76
|
-
```
|
|
111
|
+
</details>
|
|
77
112
|
|
|
78
113
|
The full canary is [`examples/svelte`](../../examples/svelte) — a stock RN 0.86 app whose
|
|
79
114
|
[`App.svelte`](../../examples/svelte/App.svelte) exercises the same surface as the React, Vue, and
|
|
@@ -84,8 +119,8 @@ Angular reference canaries.
|
|
|
84
119
|
## Parity — and the one gap
|
|
85
120
|
|
|
86
121
|
Svelte reaches the same primitives, runtime modules, `Animated` on both drivers, gestures,
|
|
87
|
-
accessibility, and the `VirtualizedList` family as React, Vue, and Angular, verified
|
|
88
|
-
iOS and Android. That parity is **structural, not hand-copied**: the component logic (state
|
|
122
|
+
accessibility, and the `VirtualizedList` family as React, Vue, Solid, and Angular, verified
|
|
123
|
+
on-device on iOS and Android. That parity is **structural, not hand-copied**: the component logic (state
|
|
89
124
|
machines + render functions) is written **once** in `@symbiote-native/components`, and Svelte
|
|
90
125
|
supplies only its lifecycle — runes (`$state` / `$derived` / `$effect`) driving hand-authored
|
|
91
126
|
`.svelte` markup that mirrors each `render-*.ts` directly, rather than a generic descriptor bridge
|
|
@@ -124,9 +159,9 @@ guarding it.
|
|
|
124
159
|
|
|
125
160
|
## Run it
|
|
126
161
|
|
|
127
|
-
[`examples/svelte`](../../examples/svelte) is a stock React Native 0.86 app. Requires Node ≥ 22
|
|
128
|
-
and the [RN environment
|
|
129
|
-
CocoaPods):
|
|
162
|
+
[`examples/svelte`](../../examples/svelte) is a stock React Native 0.86 app. Requires Node ≥ 22.13
|
|
163
|
+
(react-native 0.86's own `package.json#engines`) and the [RN environment
|
|
164
|
+
setup](https://reactnative.dev/docs/set-up-your-environment) (Xcode, CocoaPods):
|
|
130
165
|
|
|
131
166
|
```bash
|
|
132
167
|
cd examples/svelte
|
|
@@ -35,23 +35,26 @@
|
|
|
35
35
|
|
|
36
36
|
let rawProps: IModalProps = $props();
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Not a $derived candidate despite the $state+$effect shape below: modalReducer folds over the
|
|
39
|
+
// PREVIOUS `state` (self-referential — $derived can't read the value it's replacing) and must
|
|
40
|
+
// run POST-render (see the effect's own comment) so the keep-alive frame survives a commit.
|
|
41
|
+
let localState = $state(createInitialModalState(rawProps.visible === true));
|
|
39
42
|
|
|
40
43
|
const resolved = $derived(resolveAccessibilityProps(rawProps));
|
|
41
44
|
const isVisible = $derived(isModalVisible(resolved.visible));
|
|
42
|
-
const shouldRender = $derived(shouldRenderModal(isVisible,
|
|
45
|
+
const shouldRender = $derived(shouldRenderModal(isVisible, localState));
|
|
43
46
|
|
|
44
47
|
// Arms the iOS keep-alive on show; a hide is left to the native dismiss (state/modal.ts). The
|
|
45
48
|
// reducer is identity-stable, so the mount run triggers no extra render.
|
|
46
49
|
$effect(() => {
|
|
47
50
|
const action = modalVisibilityAction(isVisible);
|
|
48
|
-
if (action !== undefined)
|
|
51
|
+
if (action !== undefined) localState = modalReducer(localState, action);
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
// Modal.js: onDismiss is iOS-only — it drops the keep-alive, then tells the app.
|
|
52
55
|
function handleDismiss(): void {
|
|
53
56
|
if (Platform.OS !== 'ios') return;
|
|
54
|
-
|
|
57
|
+
localState = modalReducer(localState, { type: 'hide' });
|
|
55
58
|
rawProps.onDismiss?.();
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -109,6 +109,10 @@
|
|
|
109
109
|
|
|
110
110
|
// The one folded state cell — the Svelte twin of Vue's plain listState / React's stateRef.
|
|
111
111
|
const listState: IListState<ItemT> = createInitialListState<ItemT>();
|
|
112
|
+
// Plain Map by design, like listState above: separatorVersion (below) is the manual
|
|
113
|
+
// invalidation signal a read tracks — a SvelteMap would double-track and still miss nothing,
|
|
114
|
+
// but adds reactive overhead this hot path doesn't need.
|
|
115
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
112
116
|
const separatorOverrides = new Map<number, Partial<ISeparatorProps<ItemT>>>();
|
|
113
117
|
let viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
114
118
|
let batchTimer: ReturnType<typeof setTimeout> | null = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/svelte",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "SymbioteNative's Svelte adapter — a DOM shim over stock compiled Svelte output driving real native iOS/Android views through the same engine as every other adapter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"typescript": "~6.0.0",
|
|
76
|
-
"@symbiote-native/components": "3.1.
|
|
77
|
-
"@symbiote-native/css-parser": "0.5.
|
|
76
|
+
"@symbiote-native/components": "3.1.1",
|
|
77
|
+
"@symbiote-native/css-parser": "0.5.1"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"svelte": ">=5.56.0",
|
|
@@ -86,9 +86,9 @@
|
|
|
86
86
|
"react": "19.2.3",
|
|
87
87
|
"svelte": "^5.56.0",
|
|
88
88
|
"@symbiote-native/engine": "1.3.0",
|
|
89
|
-
"@symbiote-native/
|
|
90
|
-
"@symbiote-native/
|
|
91
|
-
"@symbiote-native/vue": "3.0.
|
|
89
|
+
"@symbiote-native/react": "3.0.3",
|
|
90
|
+
"@symbiote-native/test-utils": "0.4.3",
|
|
91
|
+
"@symbiote-native/vue": "3.0.3"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --build",
|