@symbiote-native/svelte 0.2.1 → 1.0.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/build/bootstrap.js +1 -1
- package/build/class-value.js +1 -1
- package/build/components/RefreshControl.svelte +4 -2
- package/build/components/Text.svelte +11 -2
- package/build/components/View.svelte +3 -1
- package/build/components/activity-indicator/index.svelte +8 -2
- package/build/components/button.svelte +8 -3
- package/build/components/flat-list/index.svelte +46 -11
- package/build/components/image/image-logic.js +13 -3
- package/build/components/image-background/index.svelte +8 -2
- package/build/components/index.d.ts +2 -2
- package/build/components/input-accessory-view/index.svelte +4 -1
- package/build/components/keyboard-avoiding-view/index.svelte +34 -11
- package/build/components/modal/index.svelte +4 -1
- package/build/components/pressable/index.svelte +48 -23
- package/build/components/pressable/index.svelte.d.ts +5 -1
- package/build/components/pressable/pressable-props.d.ts +1 -1
- package/build/components/safe-area-view-props.d.ts +1 -0
- package/build/components/scroll-view/index.svelte +54 -21
- package/build/components/scroll-view/sticky-header.svelte +49 -16
- package/build/components/section-list/index.svelte +6 -1
- package/build/components/switch/index.svelte +16 -5
- package/build/components/text-input/index.svelte +80 -8
- package/build/components/text-input/index.svelte.d.ts +5 -0
- package/build/components/touchable-highlight/index.svelte +115 -15
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +8 -3
- package/build/components/touchable-opacity/index.svelte +113 -56
- package/build/components/touchable-without-feedback/index.svelte +60 -4
- package/build/components/virtualized-list/index.svelte +233 -70
- package/build/components/virtualized-list/virtualized-list-props.js +8 -4
- package/build/components/virtualized-section-list/index.svelte +56 -9
- package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
- package/build/create-portal/index.d.ts +22 -0
- package/build/create-portal/index.js +160 -0
- package/build/descriptor-to-svelte.js +15 -21
- package/build/dom-shim/element.d.ts +9 -3
- package/build/dom-shim/element.js +77 -25
- package/build/dom-shim/fold-host-bag.d.ts +1 -0
- package/build/dom-shim/fold-host-bag.js +7 -0
- package/build/dom-shim/index.d.ts +1 -0
- package/build/dom-shim/index.js +3 -0
- package/build/dom-shim/patch-globals.js +4 -2
- package/build/dom-shim/shim-node.js +11 -0
- package/build/dom-shim/text.js +38 -1
- package/build/host-instance.js +9 -5
- package/build/index.d.ts +6 -3
- package/build/index.js +20 -11
- package/build/modules/animated/create-animated-component.d.ts +5 -0
- package/build/modules/animated/create-animated-component.js +131 -0
- package/build/modules/animated/index.d.ts +24 -14
- package/build/modules/animated/index.js +30 -30
- package/build/modules/status-bar/index.js +1 -1
- package/build/modules/status-bar/index.svelte +4 -1
- package/build/native-view-bridge.d.ts +1 -1
- package/build/native-view-bridge.js +1 -1
- package/build/preprocessor/collapse-text-whitespace.js +20 -7
- package/build/preprocessor/forbid-web-only-constructs.js +8 -2
- package/build/preprocessor/lower-host-primitives.d.ts +10 -0
- package/build/preprocessor/lower-host-primitives.js +455 -0
- package/build/preprocessor/scoped-styles.js +141 -104
- package/build/register.d.ts +1 -0
- package/build/register.js +33 -0
- package/build/render.js +48 -3
- package/build/scope-token.d.ts +3 -2
- package/build/scope-token.js +6 -17
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +8 -0
- package/build/style-scope.d.ts +2 -1
- package/build/style-scope.js +10 -8
- package/metro-svelte-transformer.cjs +48 -14
- package/package.json +28 -10
- package/build/modules/animated/AnimatedImage.svelte +0 -115
- package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedScrollView.svelte +0 -92
- package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
- package/build/modules/animated/AnimatedText.svelte +0 -58
- package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedView.svelte +0 -95
- package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
|
|
3
|
+
import { ShimElement } from '../dom-shim';
|
|
4
|
+
/**
|
|
5
|
+
* Where portaled content lands. React's `IPortalContainer` carries two members
|
|
6
|
+
* (`ISymbioteNode | SymbioteSurface`); this carries three, because Svelte's OWN handle on a
|
|
7
|
+
* mounted host node is the `ShimElement` a `bind:this` / `{@attach}` hands back — the same
|
|
8
|
+
* capability, one wrapper further out. `ISymbioteNode` stays accepted so a raw engine node
|
|
9
|
+
* (what `hostInstance()` returns, and what a third-party native-view package holds) is a target
|
|
10
|
+
* here exactly as it is on React.
|
|
11
|
+
*/
|
|
12
|
+
export type IPortalTarget = ShimElement | ISymbioteNode | SymbioteSurface;
|
|
13
|
+
export interface IPortalProps {
|
|
14
|
+
/**
|
|
15
|
+
* An already-mounted node in this surface, or the surface itself. Reactive: pointing it at a
|
|
16
|
+
* different target MOVES the content, it does not re-create it.
|
|
17
|
+
*/
|
|
18
|
+
mount: IPortalTarget;
|
|
19
|
+
/** The content to relocate. `<Portal mount={x}>…</Portal>` fills this in implicitly. */
|
|
20
|
+
children: Snippet;
|
|
21
|
+
}
|
|
22
|
+
export declare const Portal: Component<IPortalProps>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Portal — the Svelte adapter's same-surface portal: the twin of React's `createPortal`
|
|
2
|
+
// (adapters/react/src/create-portal/index.ts), Solid's `<Portal mount={…}>`
|
|
3
|
+
// (adapters/solid/src/create-portal/index.tsx) and Angular's PortalDirective/PortalOutletDirective
|
|
4
|
+
// pair. This adapter shipped `createTunnel` and no portal in any spelling until 2026-08-20; the
|
|
5
|
+
// two do NOT overlap (see the table at the bottom of this header), so the tunnel was never a
|
|
6
|
+
// substitute.
|
|
7
|
+
//
|
|
8
|
+
// WHY IT IS A COMPONENT (`<Portal mount={…}>`) AND NOT A CALL (`createPortal(content, target)`).
|
|
9
|
+
// NOT for Solid's reason. Solid had to avoid the call form because Solid evaluates JSX eagerly at
|
|
10
|
+
// the position it is written, so a call would BUILD the content before anything could relocate it.
|
|
11
|
+
// That argument does not transfer: probed against the installed svelte 5.56.8, `{#snippet
|
|
12
|
+
// children()}…{/snippet}` compiles to `const children = ($$anchor) => {…}` — a lazy closure that
|
|
13
|
+
// runs only when something calls it with an anchor, exactly like React's `children`. Svelte's
|
|
14
|
+
// reason is simpler and harder: **Svelte has no expression form for markup at all.** There is no
|
|
15
|
+
// `h()`, no JSX value; markup exists only inside a template, and the only way to hand a block of
|
|
16
|
+
// template to something else is a snippet prop. A `createPortal(snippet, target)` called from
|
|
17
|
+
// `<script>` would still have to be given a snippet — and would then need to invent its own
|
|
18
|
+
// lifetime, teardown and reactive ownership, all of which a component gets from the framework.
|
|
19
|
+
// `<Portal mount={node}>` is also the shape solid-js/web ships and the shape every community
|
|
20
|
+
// Svelte portal action ends up approximating, so a Svelte author already reads it.
|
|
21
|
+
//
|
|
22
|
+
// WHY THE BODY IS HAND-WRITTEN TS AND NOT A `.svelte` FILE — forced, not preferred. A `.svelte`
|
|
23
|
+
// template cannot express this component at all: `{@render children()}` always renders at the
|
|
24
|
+
// component's OWN anchor position, and the template language has no "render into node X" form.
|
|
25
|
+
// Choosing the destination means calling the snippet with an anchor of our own, which is
|
|
26
|
+
// precisely what the compiler's own `{@render}` does. Probed:
|
|
27
|
+
//
|
|
28
|
+
// {@render children()} -> $.snippet(node, () => $$props.children)
|
|
29
|
+
//
|
|
30
|
+
// So this file makes the SAME call the compiler emits and substitutes ONE argument: the anchor.
|
|
31
|
+
// The precedent for a hand-written component body in this adapter is
|
|
32
|
+
// modules/animated/create-animated-component.ts (see its header, and svelte-internal-client.d.ts
|
|
33
|
+
// for the narrowed declarations); the adapter is by design coupled to Svelte's private internals
|
|
34
|
+
// — the whole DOM shim is (svelte-adapter-dom-shim skill §0). A welcome side effect: `Portal` is
|
|
35
|
+
// plain TS, so it imports from ordinary TS and from vitest with no Svelte plugin in the way.
|
|
36
|
+
//
|
|
37
|
+
// SCOPE — same-surface only, matching React's boundary exactly, neither widened nor narrowed.
|
|
38
|
+
// `mount` must be an already-mounted node WITHIN THE SAME SURFACE as the Portal's call site
|
|
39
|
+
// (typically a host element you hold via `bind:this` or `{@attach}`), or that surface itself. It
|
|
40
|
+
// is not a route into a second, independently mount()ed surface: React's `resetAfterCommit` fires
|
|
41
|
+
// only for the primary root's own container, and here the equivalent is that a foreign surface
|
|
42
|
+
// would never be told to re-commit — a silent no-paint, not a crash. Cross-surface content
|
|
43
|
+
// sharing is a different mechanism: `createTunnel` (../create-tunnel).
|
|
44
|
+
//
|
|
45
|
+
// PORTAL vs TUNNEL — every row is pinned by a test, in this file's suite and the tunnel's:
|
|
46
|
+
//
|
|
47
|
+
// Reach | same surface only | any surface, incl. a separately mount()ed one
|
|
48
|
+
// Target must | no — any mounted node, | yes — a <TunnelOut/> must be rendered there
|
|
49
|
+
// cooperate | incl. one you hold a ref |
|
|
50
|
+
// Placement | the target's exact slot, | one collection point, registration order
|
|
51
|
+
// | interleaved with its own |
|
|
52
|
+
// | children |
|
|
53
|
+
// Content's | the CALL SITE (getContext | the OUT SITE (getContext resolves there)
|
|
54
|
+
// reactive owner | resolves there) |
|
|
55
|
+
// Node identity | nodes are MOVED (the old | content is re-created per TunnelOut
|
|
56
|
+
// | parent empties) |
|
|
57
|
+
import { pop, push, snippet, user_effect } from 'svelte/internal/client';
|
|
58
|
+
import { appendChild as engineAppendChild, removeChild as engineRemoveChild, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
59
|
+
import { ShimComment, ShimElement, ShimNode } from '../dom-shim/index.js';
|
|
60
|
+
// The Svelte flavour of React's `isSymbioteNode` guard, and it needs its own wording: React tells
|
|
61
|
+
// the caller to check for a forgotten `.current`, which does not exist here. A Svelte host ref is
|
|
62
|
+
// populated by an EFFECT, so it is still `null` while the template that reads it first runs —
|
|
63
|
+
// gating the Portal behind `{#if target}` is the idiomatic fix (the twin of React's callback-ref
|
|
64
|
+
// gotcha and Solid's `<Show when={…}>`).
|
|
65
|
+
function assertPortalTarget(target) {
|
|
66
|
+
if (target instanceof ShimElement ||
|
|
67
|
+
target instanceof SymbioteSurface ||
|
|
68
|
+
isSymbioteNode(target)) {
|
|
69
|
+
return target;
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Portal `mount` must be an already-mounted host node (a `bind:this` / `{@attach}` ref off a rendered component) or a surface — got something else. Is the ref still null because the template ran before the effect that fills it (gate the Portal behind `{#if target}`), or did you pass a CSS-selector-style string?');
|
|
72
|
+
}
|
|
73
|
+
function targetLabel(target) {
|
|
74
|
+
if (target instanceof SymbioteSurface)
|
|
75
|
+
return `surface#${target.rootTag}`;
|
|
76
|
+
if (target instanceof ShimElement)
|
|
77
|
+
return target.tagName;
|
|
78
|
+
return target.component;
|
|
79
|
+
}
|
|
80
|
+
// Attach the fragment host under `target` and hand back the matching detach. Three branches
|
|
81
|
+
// because the three target kinds sit at different layers:
|
|
82
|
+
//
|
|
83
|
+
// * ShimElement — attach in the SHIM tree and let it do the rest. Liveness is lazy there
|
|
84
|
+
// (shim-node.ts's makeLive), so this is the one branch that works whether or not the target
|
|
85
|
+
// has reached its first commit yet, and the host's surface arrives with it.
|
|
86
|
+
// * SymbioteSurface — no shim node exists above it, so the host is made live against the
|
|
87
|
+
// surface directly and appended as a top-level child, the same branch React's
|
|
88
|
+
// `appendChildToContainer` takes for `isSurfaceContainer`.
|
|
89
|
+
// * ISymbioteNode — a raw engine node carries no surface back-pointer, so the surface comes
|
|
90
|
+
// from the Portal's OWN call-site anchor. That is correct by construction for the only
|
|
91
|
+
// supported case: same-surface targets.
|
|
92
|
+
function attachHost(target, host, callSiteAnchor) {
|
|
93
|
+
if (target instanceof ShimElement) {
|
|
94
|
+
target.appendChild(host);
|
|
95
|
+
return () => {
|
|
96
|
+
target.removeChild(host);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (target instanceof SymbioteSurface) {
|
|
100
|
+
const node = host.makeLive(target);
|
|
101
|
+
target.appendChild(node);
|
|
102
|
+
target.requestCommit();
|
|
103
|
+
return () => {
|
|
104
|
+
target.removeChild(node);
|
|
105
|
+
target.requestCommit();
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const surface = callSiteAnchor?.surface;
|
|
109
|
+
if (surface === undefined) {
|
|
110
|
+
throw new Error('Portal `mount` was given a raw engine node, but the Portal itself is not mounted on a surface yet, so there is nothing to commit it to. Pass the `bind:this` / `{@attach}` value itself (a host element) rather than unwrapping it, or pass the surface.');
|
|
111
|
+
}
|
|
112
|
+
const node = host.makeLive(surface);
|
|
113
|
+
engineAppendChild(target, node);
|
|
114
|
+
surface.requestCommit();
|
|
115
|
+
return () => {
|
|
116
|
+
engineRemoveChild(target, node);
|
|
117
|
+
surface.requestCommit();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export const Portal = function Portal(internals, props) {
|
|
121
|
+
// The compiled twin of a component's own `$props()` scope. Without it `user_effect` below is
|
|
122
|
+
// created immediately instead of being deferred to mount, and would run before the call site
|
|
123
|
+
// has finished rendering — the same reason create-animated-component.ts opens one.
|
|
124
|
+
push(props, true);
|
|
125
|
+
// ONE engine anchor per Portal instance, as a fragment host. It is a real retained node, so
|
|
126
|
+
// the content has a stable exclusive parent to be reconciled under, and the commit walk
|
|
127
|
+
// FLATTENS an anchor's children into its parent (renderableChildren, core/engine/src/commit.ts)
|
|
128
|
+
// so the anchor itself never paints. That is what keeps portaled content a DIRECT Fabric child
|
|
129
|
+
// of the target — matching React's createPortal, and unlike a DOM portal, which always leaves
|
|
130
|
+
// its container element in the tree.
|
|
131
|
+
//
|
|
132
|
+
// It is also what makes relocation and teardown one call each: the whole subtree travels with
|
|
133
|
+
// the anchor, so nothing has to track which nodes the snippet produced.
|
|
134
|
+
const host = new ShimComment('symbiote-portal');
|
|
135
|
+
// The anchor `$.snippet` renders BEFORE — a child of the host, so the content lands inside the
|
|
136
|
+
// host rather than wherever the host currently sits. Also an engine anchor: flattened away too.
|
|
137
|
+
const renderAnchor = new ShimComment('');
|
|
138
|
+
host.appendChild(renderAnchor);
|
|
139
|
+
// A component's first argument IS its anchor node, which in this adapter is a shim node — so
|
|
140
|
+
// the Portal can read its own surface off it. Guarded rather than cast: `ComponentInternals` is
|
|
141
|
+
// an opaque branded type, and a future Svelte could hand something else.
|
|
142
|
+
const callSiteAnchor = internals instanceof ShimNode ? internals : undefined;
|
|
143
|
+
// Render FIRST, relocate second — that order is the whole reason context, error boundaries and
|
|
144
|
+
// ownership resolve from the call site: the snippet runs inside THIS component's context, and
|
|
145
|
+
// `<Portal>` is written at the call site. Only host nodes move afterwards.
|
|
146
|
+
snippet(renderAnchor, () => props.children);
|
|
147
|
+
// `props.mount` is read inside the effect, not destructured at the top: a prop arrives as a
|
|
148
|
+
// getter (probed — the compiler emits `get mount() { return $.get(x); }`), so reading it here
|
|
149
|
+
// would freeze the first target. Returning the detach as the effect's cleanup covers both
|
|
150
|
+
// cases at once — a `mount` change (fired before the effect re-attaches elsewhere) and the
|
|
151
|
+
// Portal's own teardown.
|
|
152
|
+
user_effect(() => {
|
|
153
|
+
const target = assertPortalTarget(props.mount);
|
|
154
|
+
dlog(`svelte portal -> ${targetLabel(target)}`);
|
|
155
|
+
return attachHost(target, host, callSiteAnchor);
|
|
156
|
+
});
|
|
157
|
+
// Nothing paints at the call site — React's createPortal returns a ReactPortal that renders
|
|
158
|
+
// nothing there for the same reason. A component's return value is its exports; there are none.
|
|
159
|
+
return pop({});
|
|
160
|
+
};
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
// function's actual output changed shape, which this bridge's whole cost-free model assumes
|
|
20
20
|
// never happens. If this ever fires for real, the render fn genuinely stopped being
|
|
21
21
|
// shape-stable and needs its own fix, not a workaround here.
|
|
22
|
+
import { createDescriptorShapeGuard } from '@symbiote-native/components';
|
|
22
23
|
import { getShimDocument } from './dom-shim/index.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
24
|
+
// The predicates live in @symbiote-native/components, next to the Descriptor whose contract they
|
|
25
|
+
// guard (skill §15/§19 for why this bridge depends on it at all). Solid's bridge had grown a
|
|
26
|
+
// private copy of the same checks with different coverage; one owner ends that.
|
|
27
|
+
const shape = createDescriptorShapeGuard('descriptorToSvelte');
|
|
28
28
|
function buildChild(child) {
|
|
29
29
|
const document = getShimDocument();
|
|
30
30
|
if (typeof child === 'string') {
|
|
@@ -40,22 +40,17 @@ function buildChild(child) {
|
|
|
40
40
|
return { kind: 'element', shim, children };
|
|
41
41
|
}
|
|
42
42
|
function syncChild(cached, child) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
cached.shim.data = child;
|
|
43
|
+
if (cached.kind === 'text') {
|
|
44
|
+
const text = shape.asText(child);
|
|
45
|
+
if (cached.shim.data !== text)
|
|
46
|
+
cached.shim.data = text;
|
|
48
47
|
return;
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
cached.
|
|
55
|
-
if (cached.children.length !== child.children.length) {
|
|
56
|
-
throw new Error(shapeChangedMessage('child count'));
|
|
57
|
-
}
|
|
58
|
-
cached.children.forEach((c, index) => syncChild(c, child.children[index]));
|
|
49
|
+
const element = shape.asElement(child);
|
|
50
|
+
shape.assertType(cached.shim.tagName, element.type);
|
|
51
|
+
cached.shim.p = element.props;
|
|
52
|
+
shape.assertChildCount(cached.children.length, element.children.length);
|
|
53
|
+
cached.children.forEach((c, index) => syncChild(c, element.children[index]));
|
|
59
54
|
}
|
|
60
55
|
// Materializes `descriptor.children` onto an already-live `parent` ONCE, then reuses the same
|
|
61
56
|
// shim nodes by position on every `update()`. `parent` is expected to already be live (has an
|
|
@@ -70,8 +65,7 @@ export function mountDescriptorChildren(parent, children) {
|
|
|
70
65
|
});
|
|
71
66
|
return {
|
|
72
67
|
update(next) {
|
|
73
|
-
|
|
74
|
-
throw new Error(shapeChangedMessage('root child count'));
|
|
68
|
+
shape.assertChildCount(cached.length, next.length);
|
|
75
69
|
cached.forEach((c, index) => syncChild(c, next[index]));
|
|
76
70
|
},
|
|
77
71
|
};
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
2
2
|
import { ShimNode } from './shim-node';
|
|
3
3
|
export type IShimPropBag = Record<string, unknown>;
|
|
4
|
-
export declare class
|
|
4
|
+
export declare abstract class ShimElementBase extends ShimNode {
|
|
5
|
+
}
|
|
6
|
+
export declare class ShimElement extends ShimElementBase {
|
|
5
7
|
readonly tagName: string;
|
|
6
8
|
readonly namespaceURI: string | undefined;
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
+
private attributes;
|
|
10
|
+
private domListeners;
|
|
9
11
|
private lastBag;
|
|
10
12
|
constructor(tagName: string, namespaceURI?: string);
|
|
11
13
|
get nodeName(): string;
|
|
14
|
+
private styleSlot;
|
|
15
|
+
get style(): {
|
|
16
|
+
cssText: string;
|
|
17
|
+
};
|
|
12
18
|
get p(): IShimPropBag;
|
|
13
19
|
set p(bag: IShimPropBag | undefined);
|
|
14
20
|
setAttribute(name: string, value: string): void;
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
// A per-key diff in the setter is MANDATORY (§3g(c)), not an optimization: Svelte's
|
|
11
11
|
// `set_custom_element_data` has no early-out guard and runs on every effect re-fire, so
|
|
12
12
|
// without the diff every prop gets rewritten whenever any one of them changes.
|
|
13
|
-
import { createElement, dlog,
|
|
13
|
+
import { createElement, dlog, routeProp, setEventListener, toPublicInstance, } from '@symbiote-native/engine';
|
|
14
14
|
import { descriptorFor } from '@symbiote-native/components';
|
|
15
15
|
import { normalizeSvelteClass } from '../class-value.js';
|
|
16
|
+
import { foldHostBag } from './fold-host-bag.js';
|
|
16
17
|
import { ShimNode } from './shim-node.js';
|
|
18
|
+
// The bag a not-yet-live element is diffed against in onMadeLive. A module constant rather than a
|
|
19
|
+
// fresh `{}` per node: it is only ever read, and the identity lets applyBagDiff skip its second
|
|
20
|
+
// pass outright on the create path.
|
|
21
|
+
const EMPTY_BAG = Object.freeze({});
|
|
17
22
|
// Diagnostic-only: a process-wide sequence so every `set p` call across every shim element in a
|
|
18
23
|
// log dump is individually orderable against AnimatedProps reconcile#N / AnimatedView reduced#N
|
|
19
24
|
// / hostShim identity-change#N, to see which one is driving which.
|
|
@@ -21,12 +26,26 @@ let globalSetPSeq = 0;
|
|
|
21
26
|
// `on<Name>` handlers ride inside the prop bag (idiomatic Svelte 5 callback props — see the
|
|
22
27
|
// skill's §3g(c) "most of §5 collapses" note) — they are not addEventListener-style DOM
|
|
23
28
|
// listeners, so they are diffed and routed exactly like any other bag key, through routeProp.
|
|
24
|
-
|
|
29
|
+
// An empty layer that exists to be what `globalThis.Element` points at, and it is load-bearing.
|
|
30
|
+
// Svelte's `get_setters` (internal/client/dom/elements/attributes.js) walks from the ELEMENT
|
|
31
|
+
// INSTANCE up and STOPS when it reaches `Element.prototype`. While `Element` was `ShimElement`
|
|
32
|
+
// itself, the first prototype step was already the stop, so the walk collected nothing and `p` —
|
|
33
|
+
// which lives on `ShimElement.prototype`, one step past it — was invisible. `set_attributes` then
|
|
34
|
+
// fell through to `setAttribute`, which writes an inert Map, and every prop vanished with nothing
|
|
35
|
+
// red. The rule is therefore that `Element` must be a proper ANCESTOR of the class owning the
|
|
36
|
+
// setters, never that class: `.claude/rules/svelte-shim-element-global-must-be-an-ancestor.md`.
|
|
37
|
+
export class ShimElementBase extends ShimNode {
|
|
38
|
+
}
|
|
39
|
+
export class ShimElement extends ShimElementBase {
|
|
25
40
|
tagName;
|
|
26
41
|
namespaceURI;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
// Both LAZY, and that is a measured decision, not a style one. A lowered primitive carries its
|
|
43
|
+
// whole prop surface in the `p` bag, so neither map is ever written on the create path — but an
|
|
44
|
+
// eager field allocated two Maps per element regardless, 18 004 of them on a 1 000-row create,
|
|
45
|
+
// in a window where GC is the largest single bucket (29%).
|
|
46
|
+
attributes;
|
|
47
|
+
domListeners;
|
|
48
|
+
lastBag = EMPTY_BAG;
|
|
30
49
|
constructor(tagName, namespaceURI) {
|
|
31
50
|
super();
|
|
32
51
|
this.tagName = tagName;
|
|
@@ -35,66 +54,85 @@ export class ShimElement extends ShimNode {
|
|
|
35
54
|
get nodeName() {
|
|
36
55
|
return this.tagName;
|
|
37
56
|
}
|
|
57
|
+
// `set_style` writes `dom.style.cssText`, so a shim with no `.style` THROWS rather than
|
|
58
|
+
// no-opping — a `style` attribute on a bare tag crashed the mount. LAZY, for the reason
|
|
59
|
+
// `.claude/rules/svelte-shim-is-the-per-node-create-path.md` records about the two Maps below it:
|
|
60
|
+
// an eager field is one object per element, ~9 000 per create, in the window where GC is the
|
|
61
|
+
// largest bucket. Only `set_style` touches this, and no lowered element takes that path.
|
|
62
|
+
styleSlot;
|
|
63
|
+
get style() {
|
|
64
|
+
return (this.styleSlot ??= { cssText: '' });
|
|
65
|
+
}
|
|
38
66
|
// The single object-bag prop. The literal name is ours to choose (§3g(c)) — the adapter's
|
|
39
67
|
// own View.svelte/Text.svelte/… emit `<symbiote-view p={bag}>`; app code never sees it.
|
|
40
68
|
get p() {
|
|
41
69
|
return this.lastBag;
|
|
42
70
|
}
|
|
43
71
|
set p(bag) {
|
|
44
|
-
|
|
72
|
+
// The fold runs HERE rather than at element creation because an alias can arrive on an update
|
|
73
|
+
// (`id` bound to a signal), and it runs before the diff so `lastBag` is always the folded shape
|
|
74
|
+
// — otherwise a seeded default would look like a change on every single set.
|
|
75
|
+
const next = foldHostBag(this.tagName, normalizeBagClasses(bag ?? {}));
|
|
45
76
|
const prev = this.lastBag;
|
|
46
77
|
this.lastBag = next;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
// THUNK, not a string (see debug.ts's header): this setter runs once per element per create
|
|
79
|
+
// and the changed-key list is diagnostic only. Built eagerly it cost a Set, two key arrays, a
|
|
80
|
+
// join and a template literal on every one of 9 002 elements, with logging off.
|
|
81
|
+
dlog(() => `ShimElement set-p#${++globalSetPSeq} tag=${this.tagName} live=${this.engineNode !== undefined} ` +
|
|
82
|
+
`changedKeys=${diffKeys(prev, next).join(',')}`);
|
|
50
83
|
if (this.engineNode === undefined)
|
|
51
84
|
return; // not live yet — onMadeLive() replays `next` in full
|
|
52
85
|
applyBagDiff(this.engineNode, prev, next);
|
|
53
86
|
this.surface?.requestCommit();
|
|
54
87
|
}
|
|
55
88
|
setAttribute(name, value) {
|
|
56
|
-
this.attributes.set(name, value);
|
|
89
|
+
(this.attributes ??= new Map()).set(name, value);
|
|
57
90
|
}
|
|
58
91
|
getAttribute(name) {
|
|
59
|
-
return this.attributes
|
|
92
|
+
return this.attributes?.get(name) ?? null;
|
|
60
93
|
}
|
|
61
94
|
removeAttribute(name) {
|
|
62
|
-
this.attributes
|
|
95
|
+
this.attributes?.delete(name);
|
|
63
96
|
}
|
|
64
97
|
addEventListener(name, handler) {
|
|
65
|
-
this.domListeners.set(name, handler);
|
|
98
|
+
(this.domListeners ??= new Map()).set(name, handler);
|
|
66
99
|
if (this.engineNode !== undefined)
|
|
67
100
|
setEventListener(this.engineNode, name, handler);
|
|
68
101
|
}
|
|
69
102
|
removeEventListener(name) {
|
|
70
|
-
this.domListeners
|
|
103
|
+
this.domListeners?.delete(name);
|
|
71
104
|
if (this.engineNode !== undefined)
|
|
72
105
|
setEventListener(this.engineNode, name, undefined);
|
|
73
106
|
}
|
|
74
107
|
cloneNode(deep) {
|
|
75
108
|
const clone = new ShimElement(this.tagName, this.namespaceURI);
|
|
76
|
-
|
|
77
|
-
clone.attributes.
|
|
109
|
+
if (this.attributes !== undefined)
|
|
110
|
+
clone.attributes = new Map(this.attributes);
|
|
78
111
|
if (deep === true) {
|
|
79
112
|
for (const child of this.children)
|
|
80
113
|
clone.appendChild(child.cloneNode(true));
|
|
81
114
|
}
|
|
82
115
|
return clone;
|
|
83
116
|
}
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// getPublicInstance.
|
|
88
|
-
// the previously-bare createElement() call.
|
|
117
|
+
// measure/measureInWindow/measureLayout/setNativeProps/focus/blur — the imperative API a
|
|
118
|
+
// `bind:this` host ref hands back — ride on the engine node's prototype, so toPublicInstance is
|
|
119
|
+
// the identity and this reads the same as Vue's renderer (renderer/index.ts) and React's
|
|
120
|
+
// getPublicInstance.
|
|
89
121
|
createEngineNode() {
|
|
90
122
|
const descriptor = descriptorFor(this.tagName);
|
|
91
|
-
|
|
123
|
+
// The INTRINSIC TAG as the third argument, and this is the only place the tag alphabet still
|
|
124
|
+
// exists: `descriptor.component` is the Fabric view name (`symbiote-view` -> `RCTView`), so a
|
|
125
|
+
// host-behavior registry keyed by tag can only be reached from here. The argument defaults to
|
|
126
|
+
// `component`, so passing it changes nothing until a behavior is registered.
|
|
127
|
+
return toPublicInstance(createElement(descriptor.component, descriptor.isText, this.tagName));
|
|
92
128
|
}
|
|
93
129
|
onMadeLive() {
|
|
94
130
|
const engineNode = this.engineNode;
|
|
95
131
|
if (engineNode === undefined)
|
|
96
132
|
return;
|
|
97
|
-
applyBagDiff(engineNode,
|
|
133
|
+
applyBagDiff(engineNode, EMPTY_BAG, this.lastBag);
|
|
134
|
+
if (this.domListeners === undefined)
|
|
135
|
+
return;
|
|
98
136
|
for (const [name, handler] of this.domListeners)
|
|
99
137
|
setEventListener(engineNode, name, handler);
|
|
100
138
|
}
|
|
@@ -120,11 +158,25 @@ function normalizeBagClasses(bag) {
|
|
|
120
158
|
}
|
|
121
159
|
return next;
|
|
122
160
|
}
|
|
161
|
+
// Diagnostic only — the one caller is the `dlog` thunk above. The allocation-free twin below is
|
|
162
|
+
// what the hot path uses; keep them in step.
|
|
123
163
|
function diffKeys(prev, next) {
|
|
124
164
|
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
125
165
|
return [...keys].filter(key => prev[key] !== next[key]);
|
|
126
166
|
}
|
|
167
|
+
// Deliberately NOT `for (const key of diffKeys(...))`: that materialized a Set, two key arrays,
|
|
168
|
+
// a spread and a filtered array per element. Two direct passes route exactly the same keys —
|
|
169
|
+
// changed-or-added from `next`, then dropped keys from `prev` as `undefined`, which is what
|
|
170
|
+
// `next[key]` evaluated to in the old shape.
|
|
127
171
|
function applyBagDiff(engineNode, prev, next) {
|
|
128
|
-
for (const key of
|
|
129
|
-
|
|
172
|
+
for (const key of Object.keys(next)) {
|
|
173
|
+
if (prev[key] !== next[key])
|
|
174
|
+
routeProp(engineNode, key, next[key]);
|
|
175
|
+
}
|
|
176
|
+
if (prev === EMPTY_BAG)
|
|
177
|
+
return;
|
|
178
|
+
for (const key of Object.keys(prev)) {
|
|
179
|
+
if (!(key in next))
|
|
180
|
+
routeProp(engineNode, key, undefined);
|
|
181
|
+
}
|
|
130
182
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { foldHostBag, FOLD_PLAN_BY_TAG as BY_TAG, type IHostBag, } from '@symbiote-native/components/fold-host-bag';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Moved to `@symbiote-native/components/fold-host-bag` (2026-09-01) and kept here as a re-export.
|
|
2
|
+
//
|
|
3
|
+
// It left this adapter the moment a SECOND adapter needed it: React's wrappers are being replaced
|
|
4
|
+
// by bare intrinsic tags, and a bare tag has no wrapper to fold in — the same "third path" this
|
|
5
|
+
// file was written for, arriving in an adapter that has no lowering transform either. A per-adapter
|
|
6
|
+
// copy of a fold driven by a shared spec is the duplication `<adapters_stay_thin>` exists to stop.
|
|
7
|
+
export { foldHostBag, FOLD_PLAN_BY_TAG as BY_TAG, } from '@symbiote-native/components/fold-host-bag';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { patchGlobals, restoreGlobals } from './patch-globals';
|
|
2
2
|
export { ShimElement } from './element';
|
|
3
|
+
export { ShimComment } from './comment';
|
|
3
4
|
export { ShimNode } from './shim-node';
|
|
4
5
|
export { ShimText } from './text';
|
|
5
6
|
export { getShimDocument } from './document';
|
package/build/dom-shim/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// never sees a shim class or a `symbiote-*` tag; see the adapter's own index.ts).
|
|
3
3
|
export { patchGlobals, restoreGlobals } from './patch-globals.js';
|
|
4
4
|
export { ShimElement } from './element.js';
|
|
5
|
+
// ShimComment is the anchor path (engine createAnchor, flattened away by the commit walk) — the
|
|
6
|
+
// only shim node create-portal can use as a non-painting fragment host.
|
|
7
|
+
export { ShimComment } from './comment.js';
|
|
5
8
|
export { ShimNode } from './shim-node.js';
|
|
6
9
|
export { ShimText } from './text.js';
|
|
7
10
|
export { getShimDocument } from './document.js';
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
//
|
|
25
25
|
// `customElements` IS patched, unlike the three above — see its own comment below.
|
|
26
26
|
import { dlog } from '@symbiote-native/engine';
|
|
27
|
-
import { ShimElement } from './element.js';
|
|
27
|
+
import { ShimElement, ShimElementBase } from './element.js';
|
|
28
28
|
import { ShimText } from './text.js';
|
|
29
29
|
import { ShimComment } from './comment.js';
|
|
30
30
|
import { ShimDocumentFragment } from './document-fragment.js';
|
|
@@ -70,7 +70,9 @@ export function patchGlobals() {
|
|
|
70
70
|
customElements: g.customElements,
|
|
71
71
|
};
|
|
72
72
|
g.Node = ShimNode;
|
|
73
|
-
|
|
73
|
+
// ShimElementBase, NOT ShimElement: `get_setters` stops AT `Element.prototype`, so pointing this
|
|
74
|
+
// at the class that owns `p` hides `p` from every `set_attributes` caller. See element.ts.
|
|
75
|
+
g.Element = ShimElementBase;
|
|
74
76
|
// Svelte's mandatory paths never distinguish HTMLElement/SVGElement from Element (we have
|
|
75
77
|
// no `<svg>` primitives), so both alias the same class — real, extensible prototypes either
|
|
76
78
|
// way, which is all `init_operations()` requires (§3a).
|
|
@@ -69,12 +69,23 @@ export class ShimNode {
|
|
|
69
69
|
for (const node of nodes)
|
|
70
70
|
this.appendChild(node);
|
|
71
71
|
}
|
|
72
|
+
// The fragment branch is the rare one — every ordinary insert is a single node — but
|
|
73
|
+
// `normalizeInsertable` allocated a one-element array for it regardless, 23 006 of them on a
|
|
74
|
+
// 1 000-row create. Test the flag first and hand the node straight to insertOne.
|
|
72
75
|
appendChild(node) {
|
|
76
|
+
if (!node.isDocumentFragment) {
|
|
77
|
+
this.insertOne(node, null);
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
73
80
|
for (const single of normalizeInsertable(node))
|
|
74
81
|
this.insertOne(single, null);
|
|
75
82
|
return node;
|
|
76
83
|
}
|
|
77
84
|
insertBefore(node, ref) {
|
|
85
|
+
if (!node.isDocumentFragment) {
|
|
86
|
+
this.insertOne(node, ref);
|
|
87
|
+
return node;
|
|
88
|
+
}
|
|
78
89
|
for (const single of normalizeInsertable(node))
|
|
79
90
|
this.insertOne(single, ref);
|
|
80
91
|
return node;
|
package/build/dom-shim/text.js
CHANGED
|
@@ -18,6 +18,39 @@
|
|
|
18
18
|
// native-node-parity.test.ts, which diffs committed native trees against the Vue adapter.
|
|
19
19
|
import { appendChild as engineAppendChild, createAnchor, createRawText, insertBefore as engineInsertBefore, isAnchor, removeChild as engineRemoveChild, setText, } from '@symbiote-native/engine';
|
|
20
20
|
import { ShimNode } from './shim-node.js';
|
|
21
|
+
// Deliberately WIDER than Svelte's own whitespace class. `svelte/src/compiler/phases/patterns.js`
|
|
22
|
+
// uses /[^ \t\r\n]/ and says why: "Not \S because that also removes explicit whitespace defined
|
|
23
|
+
// through things like ` `". For Svelte the character IS the discriminator, so it must keep
|
|
24
|
+
// an author's deliberate nbsp. For us the PARENT is the discriminator, and it has already proved
|
|
25
|
+
// the node is unrenderable - a raw text under a non-text parent cannot paint whatever character
|
|
26
|
+
// it holds. So ` `, ` `, \f, \v, U+2028, U+3000 and the zero-width family (which `\s`
|
|
27
|
+
// misses) all drop here, while a deliberate nbsp INSIDE a <Text> is kept by the parent check.
|
|
28
|
+
// Measured: each of these arrives as its own text node in the from_tree template.
|
|
29
|
+
const WHITESPACE_ONLY = /^[\s\u200b-\u200d\ufeff]+$/;
|
|
30
|
+
// Whitespace-only text under a parent that cannot hold raw text is FORMATTING, not content: the
|
|
31
|
+
// gap Svelte leaves between two sibling tags written on separate lines. Svelte collapses every
|
|
32
|
+
// such run to a single ' ' but never deletes it — in the DOM that space separates inline words
|
|
33
|
+
// and only CSS decides whether it paints. Fabric has no such layer, so a raw text outside a
|
|
34
|
+
// <Text> is simply invalid: the same invariant the engine enforces at commit time.
|
|
35
|
+
//
|
|
36
|
+
// The PARENT is what makes this exact rather than a heuristic. Measured on svelte 5.56.8, a
|
|
37
|
+
// stray gap and an {#each} text placeholder are the same ' ' string in the from_tree template:
|
|
38
|
+
//
|
|
39
|
+
// stray gap ['symbiote-view', null, [...], ' ', [...]] parent takes no raw text -> drop
|
|
40
|
+
// placeholder ['symbiote-text', null, ' '] parent IS a <Text> -> keep
|
|
41
|
+
//
|
|
42
|
+
// So `<Text><Text>a</Text> <Text>b</Text></Text>` keeps its separator, correctly — there the
|
|
43
|
+
// space really is a word boundary. This also covers the one shape the source preprocessor
|
|
44
|
+
// admits it cannot catch (two siblings, one line, no newline): by here Svelte has already
|
|
45
|
+
// normalized that form to the very same single space.
|
|
46
|
+
//
|
|
47
|
+
// makeLive() binds a parent before its children, so the parent's engine node is always present
|
|
48
|
+
// by the time this runs; a fragment is unwrapped into the real parent before insertion.
|
|
49
|
+
function isFormattingWhitespace(value, parent) {
|
|
50
|
+
if (!WHITESPACE_ONLY.test(value))
|
|
51
|
+
return false;
|
|
52
|
+
return parent?.engineNode?.isText !== true;
|
|
53
|
+
}
|
|
21
54
|
export class ShimText extends ShimNode {
|
|
22
55
|
value;
|
|
23
56
|
constructor(value) {
|
|
@@ -92,6 +125,10 @@ export class ShimText extends ShimNode {
|
|
|
92
125
|
return new ShimText(this.value);
|
|
93
126
|
}
|
|
94
127
|
createEngineNode() {
|
|
95
|
-
|
|
128
|
+
if (this.value === '')
|
|
129
|
+
return createAnchor();
|
|
130
|
+
if (isFormattingWhitespace(this.value, this.parent))
|
|
131
|
+
return createAnchor();
|
|
132
|
+
return createRawText(this.value);
|
|
96
133
|
}
|
|
97
134
|
}
|
package/build/host-instance.js
CHANGED
|
@@ -15,20 +15,24 @@ import { getNativeTag, isSymbioteNode, toPublicInstance, dlog, } from '@symbiote
|
|
|
15
15
|
// and none of those is a host ref an interop library can hand back. `tagName` is ShimElement's
|
|
16
16
|
// own field, so checking it makes the predicate mean what its name says.
|
|
17
17
|
function isShimElement(value) {
|
|
18
|
-
return typeof value === 'object' &&
|
|
18
|
+
return (typeof value === 'object' &&
|
|
19
|
+
value !== null &&
|
|
20
|
+
'engineNode' in value &&
|
|
21
|
+
'tagName' in value);
|
|
19
22
|
}
|
|
20
23
|
// The typed imperative handle (measure/measureInWindow/measureLayout/setNativeProps/focus/blur)
|
|
21
24
|
// a `bind:this` host ref carries — the Svelte twin of a React `ref.current`/Vue template ref
|
|
22
|
-
// already being an `IHostInstance`.
|
|
23
|
-
// toPublicInstance
|
|
24
|
-
// isHostInstance guard short-circuits); this helper exists only to give app code a correctly
|
|
25
|
+
// already being an `IHostInstance`. Every engine node carries those methods on its prototype, so
|
|
26
|
+
// the toPublicInstance call below is the identity; this helper exists only to give app code a correctly
|
|
25
27
|
// TYPED accessor off the SHIM value (`ShimElement`) instead of the bare `.engineNode` field,
|
|
26
28
|
// with no `as` cast at the call site.
|
|
27
29
|
export function hostInstance(shim) {
|
|
28
30
|
if (shim === null || shim === undefined)
|
|
29
31
|
return undefined;
|
|
30
32
|
const node = shim.engineNode;
|
|
31
|
-
return node !== undefined && isSymbioteNode(node)
|
|
33
|
+
return node !== undefined && isSymbioteNode(node)
|
|
34
|
+
? toPublicInstance(node)
|
|
35
|
+
: undefined;
|
|
32
36
|
}
|
|
33
37
|
export function findNodeHandle(componentOrHandle) {
|
|
34
38
|
if (componentOrHandle === null || componentOrHandle === undefined)
|
package/build/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './register';
|
|
1
2
|
export { mount, unmount } from './render';
|
|
2
3
|
export { AppRegistry, setHostRegistrar } from './modules/app-registry';
|
|
3
4
|
export type { IComponentProvider, IWrapperComponentProvider, IAppParameters, IRunnable, IHostRegistrar, IRegistry, IHeadlessTask, ITaskProvider, ITaskCanceller, ITaskCancelProvider, } from './modules/app-registry';
|
|
@@ -17,12 +18,14 @@ export type { IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessi
|
|
|
17
18
|
export { findNodeHandle, hostInstance } from './host-instance';
|
|
18
19
|
export type { IHostInstance } from './host-instance';
|
|
19
20
|
export type { ShimElement } from './dom-shim';
|
|
20
|
-
export { createTunnel, TunnelIn, TunnelOut, type ITunnel } from './create-tunnel';
|
|
21
|
+
export { createTunnel, TunnelIn, TunnelOut, type ITunnel, } from './create-tunnel';
|
|
22
|
+
export { Portal, type IPortalProps, type IPortalTarget } from './create-portal';
|
|
21
23
|
export { useWindowDimensions } from './runes/use-window-dimensions.svelte';
|
|
22
24
|
export { useColorScheme } from './runes/use-color-scheme.svelte';
|
|
23
|
-
export { innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio } from './runes/window';
|
|
25
|
+
export { innerWidth, innerHeight, outerWidth, outerHeight, devicePixelRatio, } from './runes/window';
|
|
24
26
|
export { orientation, createWidthQuery } from './runes/media-query';
|
|
25
27
|
export type { IOrientation, IWidthQueryBounds } from './runes/media-query';
|
|
26
28
|
export type { IReactiveValue } from './runes/dimensions-value';
|
|
27
29
|
export { normalizeSvelteClass, resolveSvelteClass, type ISvelteClassValue, type IClassMap, type IClassEntry, } from './class-value';
|
|
28
|
-
export { Animated } from './modules/animated';
|
|
30
|
+
export { Animated, createAnimatedComponent } from './modules/animated';
|
|
31
|
+
export type { IAnimatedComponentProps } from './modules/animated';
|