@symbiote-native/vue 1.0.0 → 2.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/README.md +9 -9
- package/babel-jsx.cjs +7 -8
- package/build/components/activity-indicator-props.d.ts +5 -0
- package/build/components/activity-indicator-props.js +1 -0
- package/build/components/button-props.d.ts +5 -0
- package/build/components/button-props.js +1 -0
- package/build/components/flat-list/index.d.ts +3 -3
- package/build/components/flat-list/index.js +2 -2
- package/build/components/image-background-props.d.ts +7 -0
- package/build/components/image-background-props.js +1 -0
- package/build/components/image-props.d.ts +5 -0
- package/build/components/image-props.js +5 -0
- package/build/components/{input-accessory-view.d.ts → input-accessory-view-props.d.ts} +1 -3
- package/build/components/input-accessory-view-props.js +5 -0
- package/build/components/keyboard-avoiding-view.js +3 -3
- package/build/components/modal/index.js +1 -1
- package/build/components/pressable-props.d.ts +29 -0
- package/build/components/pressable-props.js +12 -0
- package/build/components/refresh-control-props.d.ts +16 -0
- package/build/components/refresh-control-props.js +9 -0
- package/build/components/safe-area-view-props.d.ts +6 -0
- package/build/components/safe-area-view-props.js +5 -0
- package/build/components/scroll-view/{shared.d.ts → scroll-view-props.d.ts} +4 -27
- package/build/components/scroll-view/scroll-view-props.js +1 -0
- package/build/components/section-list/index.d.ts +3 -3
- package/build/components/switch/switch-props.d.ts +6 -0
- package/build/components/switch/switch-props.js +1 -0
- package/build/components/text-input/text-input-props.d.ts +5 -0
- package/build/components/text-input/text-input-props.js +1 -0
- package/build/components/text-props.d.ts +24 -0
- package/build/components/text-props.js +6 -0
- package/build/components/touchable-native-feedback-props.d.ts +6 -0
- package/build/components/touchable-native-feedback-props.js +1 -0
- package/build/components/touchable-props.d.ts +15 -0
- package/build/components/touchable-props.js +1 -0
- package/build/components/touchable-without-feedback-props.d.ts +3 -0
- package/build/components/touchable-without-feedback-props.js +1 -0
- package/build/components/view-props.d.ts +29 -0
- package/build/components/view-props.js +5 -0
- package/build/components/virtualized-list/index.d.ts +3 -3
- package/build/components/virtualized-list/index.js +42 -37
- package/build/components/virtualized-section-list/index.d.ts +3 -3
- package/build/descriptor-to-vue.js +2 -2
- package/build/index.d.ts +24 -27
- package/build/index.js +31 -16
- package/build/intrinsic-elements.d.ts +19 -0
- package/build/intrinsic-elements.js +1 -0
- package/build/jsx-runtime.d.ts +67 -0
- package/build/jsx-runtime.js +1 -0
- package/build/modules/animated/index.d.ts +3 -1
- package/build/modules/animated/index.js +20 -19
- package/build/modules/image/index.d.ts +3 -0
- package/build/modules/image/index.js +13 -0
- package/build/register.d.ts +1 -1
- package/build/register.js +13 -33
- package/build/renderer/index.js +58 -7
- package/build/runtime-helpers/index.d.ts +10 -1
- package/build/runtime-helpers/index.js +47 -11
- package/intrinsic-tags.cjs +52 -0
- package/metro-vue-transformer.cjs +55 -345
- package/package.json +12 -8
- package/babel-lower-host-primitives.cjs +0 -225
- package/build/components/activity-indicator/index.android.d.ts +0 -4
- package/build/components/activity-indicator/index.android.js +0 -10
- package/build/components/activity-indicator/index.d.ts +0 -1
- package/build/components/activity-indicator/index.ios.d.ts +0 -4
- package/build/components/activity-indicator/index.ios.js +0 -10
- package/build/components/activity-indicator/index.js +0 -4
- package/build/components/activity-indicator/shared.d.ts +0 -8
- package/build/components/activity-indicator/shared.js +0 -48
- package/build/components/button.d.ts +0 -14
- package/build/components/button.js +0 -61
- package/build/components/image-background.d.ts +0 -9
- package/build/components/image-background.js +0 -103
- package/build/components/image.d.ts +0 -9
- package/build/components/image.js +0 -90
- package/build/components/input-accessory-view.js +0 -41
- package/build/components/pressable.d.ts +0 -58
- package/build/components/pressable.js +0 -233
- package/build/components/refresh-control.d.ts +0 -20
- package/build/components/refresh-control.js +0 -47
- package/build/components/safe-area-view.d.ts +0 -9
- package/build/components/safe-area-view.js +0 -33
- package/build/components/scroll-view/index.android.d.ts +0 -4
- package/build/components/scroll-view/index.android.js +0 -64
- package/build/components/scroll-view/index.d.ts +0 -1
- package/build/components/scroll-view/index.ios.d.ts +0 -4
- package/build/components/scroll-view/index.ios.js +0 -17
- package/build/components/scroll-view/index.js +0 -4
- package/build/components/scroll-view/shared.js +0 -271
- package/build/components/scroll-view/sticky-header.d.ts +0 -7
- package/build/components/scroll-view/sticky-header.js +0 -208
- package/build/components/switch/index.android.d.ts +0 -12
- package/build/components/switch/index.android.js +0 -13
- package/build/components/switch/index.d.ts +0 -2
- package/build/components/switch/index.ios.d.ts +0 -12
- package/build/components/switch/index.ios.js +0 -11
- package/build/components/switch/index.js +0 -5
- package/build/components/switch/shared.d.ts +0 -23
- package/build/components/switch/shared.js +0 -126
- package/build/components/text-input/index.d.ts +0 -24
- package/build/components/text-input/index.js +0 -260
- package/build/components/touchable-native-feedback.d.ts +0 -24
- package/build/components/touchable-native-feedback.js +0 -82
- package/build/components/touchable.d.ts +0 -50
- package/build/components/touchable.js +0 -281
- package/build/components.d.ts +0 -53
- package/build/components.js +0 -45
- package/build/state-style.d.ts +0 -1
- package/build/state-style.js +0 -13
package/build/register.js
CHANGED
|
@@ -1,35 +1,15 @@
|
|
|
1
|
-
// Side-effect ONLY.
|
|
1
|
+
// Side-effect ONLY. Exports nothing, and must never be re-exported from the barrel.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `import './register'
|
|
3
|
+
// The fourteen calls live once, in `@symbiote-native/components/register` — they were identical in
|
|
4
|
+
// all five adapters, and each copy carried a migration ledger justifying a call against a wrapper
|
|
5
|
+
// that no longer exists. What is per-adapter is only the SPELLING of the import below: every
|
|
6
|
+
// `*-tag.test.*` here reaches the registry through `import './register';`, and this adapter's own
|
|
7
|
+
// barrel guard pins that line.
|
|
7
8
|
//
|
|
8
|
-
// WHY
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
// Babel merges the two into one dependency and the merged one stays lazy. See CLAUDE.md, "Never
|
|
16
|
-
// make correctness depend on a module's load-time side effect".
|
|
17
|
-
//
|
|
18
|
-
// `adapters/vue/src/index.test.ts` pins the barrel against both halves of that.
|
|
19
|
-
import { registerImageBehavior, registerInputAccessoryViewBehavior, registerPressableBehavior, registerSwitchBehavior, registerTextInputBehavior, } from '@symbiote-native/components';
|
|
20
|
-
registerPressableBehavior();
|
|
21
|
-
// Only the LOWERED tags carry this — the wrapper renders `symbiote-text-input-managed` and
|
|
22
|
-
// keeps running its own lifecycle. One owner per node; see `component-names/shared.ts`.
|
|
23
|
-
registerTextInputBehavior();
|
|
24
|
-
// Same reason as TextInput: the wrapper renders `symbiote-switch-managed` and runs its own
|
|
25
|
-
// lastNativeReport/snap-back lifecycle, so the engine's copy attaches only to the bare tag.
|
|
26
|
-
registerSwitchBehavior();
|
|
27
|
-
// Image owns no runtime — its behavior is a prop FOLD and nothing else, and it is registered on the
|
|
28
|
-
// same `symbiote-image` the wrapper already emits rather than on a `-managed` twin. That is safe
|
|
29
|
-
// only because the mapping is idempotent, which `core/components/src/behaviors/image.test.ts`
|
|
30
|
-
// asserts rather than assumes; a wrapper-built node simply folds a second time and nothing moves.
|
|
31
|
-
registerImageBehavior();
|
|
32
|
-
// Fold-only, and it shares the wrapper's tag for the same reason Image does: the mapping has no
|
|
33
|
-
// aliasing at all, so a wrapper-built node folding a second time moves nothing.
|
|
34
|
-
// `core/components/src/behaviors/input-accessory-view.test.ts` asserts that rather than assuming it.
|
|
35
|
-
registerInputAccessoryViewBehavior();
|
|
9
|
+
// WHY A SIDE-EFFECT SUBPATH AND NOT A BARREL EXPORT. Metro turns on `inlineRequires` for PRODUCTION
|
|
10
|
+
// only: it moves a `require` down to the first place its binding is used as a VALUE, and
|
|
11
|
+
// `export { X } from './x'` compiles to a lazy getter. A registration reached that way never
|
|
12
|
+
// evaluates in a Release build — invisible to tsc, to vitest, and to grepping the bundle. A bare
|
|
13
|
+
// `import` placed NEXT TO a re-export of the same specifier does not help either: Babel merges the
|
|
14
|
+
// two into one dependency and the merged dependency stays lazy.
|
|
15
|
+
import '@symbiote-native/components/register';
|
package/build/renderer/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the engine's tiny mutation API; the engine owns all Fabric clone-on-write, so Vue
|
|
3
3
|
// drives the exact same retained tree React does: the proof the core is framework-
|
|
4
4
|
// agnostic.
|
|
5
|
-
import { createRenderer } from '@vue/runtime-core';
|
|
5
|
+
import { callWithErrorHandling, createRenderer, ErrorCodes, markRaw, } from '@vue/runtime-core';
|
|
6
6
|
import { appendChild, createAnchor, createElement, createRawText, dlog, insertBefore, removeChild, routeProp, setProp, setText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
|
|
7
7
|
import { descriptorFor } from '@symbiote-native/components';
|
|
8
8
|
import { normalizeVueAttrKey } from '../utils/normalize-attrs.js';
|
|
@@ -15,7 +15,7 @@ function isRawText(node) {
|
|
|
15
15
|
// RN's Text.js applies two defaults on the way to native (core/components/src/text-props.ts:
|
|
16
16
|
// ellipsizeMode 'tail', allowFontScaling true unless literally false). The Vue <Text> wrapper
|
|
17
17
|
// folded them with resolveTextProps; a template that the SFC transformer lowered to the
|
|
18
|
-
// intrinsic `
|
|
18
|
+
// intrinsic `text` has no wrapper, so the renderer seeds them instead. Without this a
|
|
19
19
|
// numberOfLines={1} line clips mid-word with no ellipsis — device-observed, and silent.
|
|
20
20
|
const TEXT_DEFAULTS = new Map([
|
|
21
21
|
['ellipsizeMode', 'tail'],
|
|
@@ -31,13 +31,51 @@ function seedTextDefaults(node) {
|
|
|
31
31
|
// id="x">` reached Fabric with an unknown `id` and no `nativeID`, silently and on device only.
|
|
32
32
|
// It lives in the renderer rather than in a transform because that covers all four Vue paths at
|
|
33
33
|
// once — lowered SFC, lowered TSX, the component wrapper, and a hand-written
|
|
34
|
-
// `h('
|
|
34
|
+
// `h('view', { id })` no compiler ever sees.
|
|
35
35
|
//
|
|
36
36
|
// Caveat, and it matches what Solid's compile-time rename already does: with BOTH `id` and
|
|
37
37
|
// `nativeID` on one element the last patchProp wins, where upstream gives `id` priority
|
|
38
38
|
// unconditionally. Honouring that needs per-node state to remember an `id` arrived; no example or
|
|
39
39
|
// test sets both, so the state is not worth carrying.
|
|
40
40
|
const PROP_ALIASES = new Map([['id', 'nativeID']]);
|
|
41
|
+
// RN-style event prop naming ('onPress', 'onValueChange', ...), the same convention JSX itself
|
|
42
|
+
// uses to separate an event from a plain value prop — good enough to decide whether to wrap,
|
|
43
|
+
// without duplicating routeProp's own ViewConfig-derived event lists here.
|
|
44
|
+
const EVENT_PROP_NAME = /^on[A-Z]/;
|
|
45
|
+
// A native event (a Fabric touch, a responder gesture) invokes a Pressable/TextInput/etc.
|
|
46
|
+
// listener straight out of the engine's own event dispatch (core/engine/src/dispatch.ts,
|
|
47
|
+
// core/components/src/behaviors/pressable.ts's `dispatch`), entirely outside anything Vue ever
|
|
48
|
+
// wraps — no render, no watcher, no lifecycle hook sits between the native event and the app's
|
|
49
|
+
// callback. So a throw inside `onPress={throwNow}` was a genuinely UNCAUGHT JS exception: it
|
|
50
|
+
// skipped `onErrorCaptured`, skipped `app.config.errorHandler`, skipped even this adapter's own
|
|
51
|
+
// default reporter (render.ts's `reportToHost`) and reached Hermes's own top-level handler
|
|
52
|
+
// directly — the raw, component-stack-less redbox this was device-reported as, not the "vue
|
|
53
|
+
// render (...)" framed one `reportToHost` produces. Real Vue DOM has the identical seam
|
|
54
|
+
// (`patchEvent`'s `createInvoker`) for the identical reason: a DOM event is native too.
|
|
55
|
+
//
|
|
56
|
+
// Wrapping here, at `patchProp`, needs the OWNING instance — and `getCurrentInstance()` cannot
|
|
57
|
+
// supply it: Vue resets `currentRenderingInstance` at the END of `renderComponentRoot`, BEFORE
|
|
58
|
+
// the subsequent `patch()` call that actually reaches `mountElement`/`patchProp`, so by the time
|
|
59
|
+
// this runs it always reads null (measured via a real thrown press listener — `handleError` never
|
|
60
|
+
// found the `onErrorCaptured` ancestor because `if (instance)` was false). Real Vue DOM does not
|
|
61
|
+
// use `getCurrentInstance()` for this either: `hostPatchProp` is called with an explicit 6th
|
|
62
|
+
// argument, `parentComponent`, threaded down through `patch`/`mountElement` from the component
|
|
63
|
+
// whose render produced the vnode — the same instance the DOM renderer's own `patchEvent` passes
|
|
64
|
+
// to `callWithAsyncErrorHandling`. That parameter, not the global pointer, is the correct capture.
|
|
65
|
+
//
|
|
66
|
+
// `callWithErrorHandling` (the SYNC form, not `callWithAsyncErrorHandling`) is deliberate: a
|
|
67
|
+
// responder-negotiation callback (`onStartShouldSetResponder`, ...) returns a boolean the engine
|
|
68
|
+
// reads synchronously, and the sync form is the one that returns the call's result directly
|
|
69
|
+
// rather than folding it into promise-handling for the multi-hook case.
|
|
70
|
+
function wrapListenerForErrorHandling(listener, instance) {
|
|
71
|
+
const wrapper = (...args) => callWithErrorHandling(listener, instance, ErrorCodes.NATIVE_EVENT_HANDLER, args);
|
|
72
|
+
// Some listeners are marker-carrying functions, not plain callbacks — Animated.event's
|
|
73
|
+
// __getHandler() (core/engine/src/animated/event.ts) hands back exactly this shape,
|
|
74
|
+
// `Object.assign((...) => {...}, { __getEvent: () => this })`, and the engine's native-driver
|
|
75
|
+
// attach path reads `__getEvent` off the prop value it was given. A fresh closure with none of
|
|
76
|
+
// the original's own properties silently breaks that — carry them forward.
|
|
77
|
+
return Object.assign(wrapper, listener);
|
|
78
|
+
}
|
|
41
79
|
// An explicit `undefined` must NOT clear one of those defaults: RN treats a missing prop and an
|
|
42
80
|
// explicit undefined alike, and only a literal `false` opts out of allowFontScaling. Reached
|
|
43
81
|
// only when a value is already undefined, so it costs nothing on the hot path.
|
|
@@ -53,7 +91,7 @@ export function createSymbioteRenderer(surface) {
|
|
|
53
91
|
createElement(type) {
|
|
54
92
|
const descriptor = descriptorFor(type);
|
|
55
93
|
// `type` as the third argument, not just `descriptor.component`: the behavior registry is
|
|
56
|
-
// keyed by the INTRINSIC TAG (`
|
|
94
|
+
// keyed by the INTRINSIC TAG (`pressable`), while a node only ever carries the
|
|
57
95
|
// resolved Fabric name (`RCTView`). This is the one place that still holds both, so a
|
|
58
96
|
// lowered primitive whose machine lives on the engine node can be matched at all.
|
|
59
97
|
const node = createElement(descriptor.component, descriptor.isText, type);
|
|
@@ -69,7 +107,16 @@ export function createSymbioteRenderer(surface) {
|
|
|
69
107
|
// one benchmark press — and a literal is built at the call site before dlog can decide
|
|
70
108
|
// anything (see core/engine/src/debug.ts).
|
|
71
109
|
dlog(() => `vue createElement ${descriptor.component} -> public instance`);
|
|
72
|
-
|
|
110
|
+
// markRaw is load-bearing, not an optimization: useTemplateRef()'s return value is
|
|
111
|
+
// `readonly(shallowRef(null))` (runtime-core.cjs.js), and Vue's readonly() wraps ANY
|
|
112
|
+
// `.value` whose Object.prototype.toString reads "[object Object]" — true of a plain class
|
|
113
|
+
// instance, unlike a real DOM Element, which fails that check for free. Unmarked, a node
|
|
114
|
+
// reached through useTemplateRef() (not a plain ref()/shallowRef(), both of which skip the
|
|
115
|
+
// wrap) came back as a deep-readonly Proxy: reads worked, so `committedOf`/`whenCommitted`
|
|
116
|
+
// saw a "committed" node, but `setNativeProps`'s `node.props.style = …` silently no-op'd
|
|
117
|
+
// with a dev-only "Set operation… target is readonly" warning — device-reported 2026-09-11
|
|
118
|
+
// as "flash the right chip" doing nothing on press.
|
|
119
|
+
return markRaw(toPublicInstance(node));
|
|
73
120
|
},
|
|
74
121
|
createText(text) {
|
|
75
122
|
// Vue mounts Fragment boundaries (v-for / v-if lists / multi-root) as EMPTY text
|
|
@@ -150,7 +197,7 @@ export function createSymbioteRenderer(surface) {
|
|
|
150
197
|
const index = siblings.indexOf(node);
|
|
151
198
|
return index >= 0 ? (siblings[index + 1] ?? null) : null;
|
|
152
199
|
},
|
|
153
|
-
patchProp(el, key, _prev, next) {
|
|
200
|
+
patchProp(el, key, _prev, next, _namespace, parentComponent) {
|
|
154
201
|
if (isSurface(el))
|
|
155
202
|
return;
|
|
156
203
|
// Kebab -> camel happens HERE, not only inside a component wrapper: the SFC transformer
|
|
@@ -159,11 +206,15 @@ export function createSymbioteRenderer(surface) {
|
|
|
159
206
|
// path, which already normalized.
|
|
160
207
|
const normalized = normalizeVueAttrKey(key);
|
|
161
208
|
const name = PROP_ALIASES.get(normalized) ?? normalized;
|
|
209
|
+
const value = next === undefined ? textDefaultFor(el, name) : next;
|
|
210
|
+
const routed = EVENT_PROP_NAME.test(name) && typeof value === 'function'
|
|
211
|
+
? wrapListenerForErrorHandling(value, parentComponent ?? null)
|
|
212
|
+
: value;
|
|
162
213
|
// routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on a
|
|
163
214
|
// View becomes a listener; onTintColor on a Switch stays a prop), shared with React. The
|
|
164
215
|
// class/style merge (explicit :style always winning, regardless of which of Vue's two
|
|
165
216
|
// independent patchProp calls lands last) is centralized there too (core/engine/src/node.ts).
|
|
166
|
-
routeProp(el, name,
|
|
217
|
+
routeProp(el, name, routed);
|
|
167
218
|
surface.requestCommit();
|
|
168
219
|
},
|
|
169
220
|
// RN has no querySelector / scope-id / innerHTML. The first two are inert; static
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from '@vue/runtime-core';
|
|
2
2
|
import { type ObjectDirective } from '@vue/runtime-core';
|
|
3
3
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
4
|
-
export { Teleport, type ITeleportTarget } from '../create-portal';
|
|
5
4
|
export declare function normalizeProps(props: Record<string, unknown> | null | undefined): Record<string, unknown> | null;
|
|
6
5
|
export declare function normalizeStyle(value: unknown): unknown;
|
|
7
6
|
export declare function mergeProps(...args: Record<string, unknown>[]): Record<string, unknown>;
|
|
@@ -28,6 +27,16 @@ type IEventHandler<TEvent> = ((event: TEvent, ...args: never[]) => unknown) & {
|
|
|
28
27
|
* typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
|
|
29
28
|
* (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
|
|
30
29
|
* type there either.
|
|
30
|
+
*
|
|
31
|
+
* `@press.self` on a LOWERED element reaches this, and both halves were measured 2026-09-11 rather
|
|
32
|
+
* than assumed — the question arose because a modifier on a COMPONENT takes Vue's own event path
|
|
33
|
+
* and only an element emits the helper. `<pressable @press.self>` compiles to
|
|
34
|
+
* `_withModifiers(fn, ["self"])` imported `from "@symbiote-native/vue/runtime-helpers"` (the Metro
|
|
35
|
+
* transformer retargets every compiled `from 'vue'`), so the compiler-emitted call lands here and
|
|
36
|
+
* not on @vue/runtime-dom's. And `ISymbioteEvent` (`core/engine/src/node.ts`) declares `target`,
|
|
37
|
+
* `currentTarget` and `stopPropagation` as REQUIRED fields with the DOM semantics these guards
|
|
38
|
+
* read, so `.self` genuinely filters a bubbled press instead of degenerating to
|
|
39
|
+
* `undefined !== undefined`.
|
|
31
40
|
*/
|
|
32
41
|
export declare function withModifiers<TEvent extends IModifierGuardableEvent>(fn: IEventHandler<TEvent>, modifiers: readonly IEventModifier[]): IEventHandler<TEvent>;
|
|
33
42
|
type IKeyedEvent = {
|
|
@@ -12,11 +12,21 @@ export * from '@vue/runtime-core';
|
|
|
12
12
|
import { getCurrentInstance, mergeProps as baseMergeProps, normalizeClass, normalizeStyle as baseNormalizeStyle, warn, } from '@vue/runtime-core';
|
|
13
13
|
import { requestCommitFor, setNativeProps, setProp, whenCommitted, } from '@symbiote-native/engine';
|
|
14
14
|
import { descriptorFor, SWITCH_TAG } from '@symbiote-native/components';
|
|
15
|
-
//
|
|
16
|
-
// same
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
15
|
+
// Do NOT shadow runtime-core's own Teleport here, however tempting the guarded ../create-portal
|
|
16
|
+
// wrapper looks (same reasoning that shadowed it in the first place, and it is wrong for this
|
|
17
|
+
// file specifically). @vue/compiler-sfc recognises the literal tag name "Teleport" as one of
|
|
18
|
+
// Vue's CORE built-ins at compile time — regardless of which value the name resolves to at
|
|
19
|
+
// runtime — and compiles its children as a raw array with a PROPS-only patchFlag (`dynamicProps:
|
|
20
|
+
// ["to"]`), never the DYNAMIC_SLOTS-flagged shape a real user component gets. Substituting a real
|
|
21
|
+
// `defineComponent` under that name makes it go through Vue's ordinary component update path
|
|
22
|
+
// (shouldUpdateComponent), which — for a PROPS-only patchFlag — checks ONLY the props named in
|
|
23
|
+
// `dynamicProps` and never re-renders on a children/slot change at all. Confirmed by reproducing
|
|
24
|
+
// through the real compiler (compileSfc) + a live mount: the wrapper's render() fires once at
|
|
25
|
+
// mount and never again, so a Teleport whose content is gated by v-if (the ordinary "toast"/
|
|
26
|
+
// "modal" shape) silently stops updating the moment `to` itself stays the same. `export *` above
|
|
27
|
+
// already re-exports the real, unwrapped Teleport for this path — leave it there. The guarded
|
|
28
|
+
// wrapper (../create-portal) is still exported from the adapter's main barrel for anyone who
|
|
29
|
+
// constructs the vnode by hand (`h(Teleport, ...)`, e.g. in TSX), where no such codegen applies.
|
|
20
30
|
// Shadows @vue/shared's own `normalizeProps` for every compiled `from 'vue'` import, for ONE
|
|
21
31
|
// divergence: a FUNCTION-valued `style` survives it.
|
|
22
32
|
//
|
|
@@ -117,6 +127,14 @@ export const vShow = {
|
|
|
117
127
|
// Drop a still-pending first-commit wait if the element unmounts before it ever lands.
|
|
118
128
|
unmounted: el => pendingShowCommits.get(el)?.(),
|
|
119
129
|
};
|
|
130
|
+
function textFromChangeEvent(event) {
|
|
131
|
+
const text = Reflect.get(event, 'text');
|
|
132
|
+
return typeof text === 'string' ? text : undefined;
|
|
133
|
+
}
|
|
134
|
+
function valueFromChangeEvent(event) {
|
|
135
|
+
const value = Reflect.get(event, 'value');
|
|
136
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
137
|
+
}
|
|
120
138
|
const modelStates = new WeakMap();
|
|
121
139
|
// Runtime guards, not casts: both slots are read out of untyped bags — `vnode.props` and
|
|
122
140
|
// `node.props` are `Record<string, unknown>` — and `typeof x === 'function'` alone narrows to the
|
|
@@ -163,9 +181,16 @@ function modelStateFor(el) {
|
|
|
163
181
|
appListener: undefined,
|
|
164
182
|
trim: false,
|
|
165
183
|
number: false,
|
|
166
|
-
listener:
|
|
167
|
-
state.appListener?.(
|
|
168
|
-
|
|
184
|
+
listener: event => {
|
|
185
|
+
state.appListener?.(event);
|
|
186
|
+
const text = textFromChangeEvent(event);
|
|
187
|
+
if (text !== undefined) {
|
|
188
|
+
state.assign?.(applyModelModifiers(state, text));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const value = valueFromChangeEvent(event);
|
|
192
|
+
if (value !== undefined)
|
|
193
|
+
state.assign?.(value);
|
|
169
194
|
},
|
|
170
195
|
};
|
|
171
196
|
modelStates.set(el, state);
|
|
@@ -182,7 +207,7 @@ function syncModelListener(el, state) {
|
|
|
182
207
|
setProp(el, 'onValueChange', state.listener);
|
|
183
208
|
}
|
|
184
209
|
// Vue's compiler picks the directive by ELEMENT, and for anything it does not recognise as a DOM
|
|
185
|
-
// input it emits `vModelText` — including a lowered `<
|
|
210
|
+
// input it emits `vModelText` — including a lowered `<switch>`. Stringifying there is what
|
|
186
211
|
// upstream must do (a DOM input's value IS a string) and what we must not: the Switch behavior
|
|
187
212
|
// reads `props.value === true`, so `String(true)` pins the control OFF and no tap can move it.
|
|
188
213
|
// Device-confirmed on `examples/vue-sfc` 2026-09-02, both switches on `CanaryScreen`.
|
|
@@ -195,8 +220,9 @@ function isSwitchNode(el) {
|
|
|
195
220
|
return el.component === SWITCH_COMPONENT;
|
|
196
221
|
}
|
|
197
222
|
function syncModelValue(el, value) {
|
|
198
|
-
// The READ half needs no branch: the behavior calls `onValueChange(
|
|
199
|
-
//
|
|
223
|
+
// The READ half needs no branch: the behavior calls `onValueChange(event)` for both primitives
|
|
224
|
+
// with `text`/`value` carried on the event, and `applyModelModifiers` passes a non-string through
|
|
225
|
+
// untouched.
|
|
200
226
|
if (isSwitchNode(el)) {
|
|
201
227
|
setProp(el, 'value', value === true);
|
|
202
228
|
requestCommitFor(el);
|
|
@@ -268,6 +294,16 @@ const MODIFIER_GUARDS = {
|
|
|
268
294
|
* typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
|
|
269
295
|
* (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
|
|
270
296
|
* type there either.
|
|
297
|
+
*
|
|
298
|
+
* `@press.self` on a LOWERED element reaches this, and both halves were measured 2026-09-11 rather
|
|
299
|
+
* than assumed — the question arose because a modifier on a COMPONENT takes Vue's own event path
|
|
300
|
+
* and only an element emits the helper. `<pressable @press.self>` compiles to
|
|
301
|
+
* `_withModifiers(fn, ["self"])` imported `from "@symbiote-native/vue/runtime-helpers"` (the Metro
|
|
302
|
+
* transformer retargets every compiled `from 'vue'`), so the compiler-emitted call lands here and
|
|
303
|
+
* not on @vue/runtime-dom's. And `ISymbioteEvent` (`core/engine/src/node.ts`) declares `target`,
|
|
304
|
+
* `currentTarget` and `stopPropagation` as REQUIRED fields with the DOM semantics these guards
|
|
305
|
+
* read, so `.self` genuinely filters a bubbled press instead of degenerating to
|
|
306
|
+
* `undefined !== undefined`.
|
|
271
307
|
*/
|
|
272
308
|
export function withModifiers(fn, modifiers) {
|
|
273
309
|
if (!fn)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Every tag both Vue compilers must treat as an ELEMENT rather than as a component to resolve.
|
|
2
|
+
//
|
|
3
|
+
// Vue decides element-vs-component in two unrelated places — `@vue/compiler-sfc`'s
|
|
4
|
+
// `isCustomElement` and `@vue/babel-plugin-jsx`'s option of the same name — so the answer has to
|
|
5
|
+
// come from one set or the two paths drift. That drift is not loud: a tag Vue takes for a component
|
|
6
|
+
// compiles to `resolveComponent("text-input")` with its children as a slot the element path never
|
|
7
|
+
// reads, which renders a blank subtree with no error.
|
|
8
|
+
//
|
|
9
|
+
// DERIVED, never enumerated. Until 2026-09-07 this was the prefix test `tag.startsWith('symbiote-')`
|
|
10
|
+
// in ./babel-jsx.cjs — correct while every intrinsic carried that marker, and answering FALSE for
|
|
11
|
+
// every tag since the prefix was dropped (`core/components/src/component-names/shared.ts` says why
|
|
12
|
+
// it went). What hid it is the tag alphabet: `view` / `text` / `image` / `switch` are real SVG
|
|
13
|
+
// element names, so both compilers resolve those four as elements with no configuration at all, and
|
|
14
|
+
// only the hyphenated half degraded.
|
|
15
|
+
//
|
|
16
|
+
// SCOPE, stated because it is smaller than `ISymbioteIntrinsic`. That union also holds
|
|
17
|
+
// `scroll-view`, `modal`, `activity-indicator` and the rest, but it is a TypeScript type and these
|
|
18
|
+
// consumers are Babel/Metro plugins that run before TS exists. `HOST_PRIMITIVES` is the only
|
|
19
|
+
// runtime-readable table reachable from here, so a hand-written `<scroll-view>` still resolves as a
|
|
20
|
+
// component. Closing that needs a CJS intrinsic table in `core/components`.
|
|
21
|
+
//
|
|
22
|
+
// WHAT THAT GAP COSTS, measured 2026-09-11 rather than reasoned, because the first guess was that
|
|
23
|
+
// the tag simply would not work. It works: `resolveComponent` falls back to the tag STRING, and
|
|
24
|
+
// `normalizeChildren` then unwraps the `withCtx` default slot for an element shapeFlag, so
|
|
25
|
+
// `<scroll-view><text>hi</text></scroll-view>` committed `RCTView > RCTScrollView > RCTText >
|
|
26
|
+
// RCTRawText`. What it costs is a dev-mode `[Vue warn]: Failed to resolve component: scroll-view`
|
|
27
|
+
// on every such element plus the component codegen path (a slot closure per element) instead of
|
|
28
|
+
// `_createElementBlock`. That is a divergence from every other tag, so `ScrollView`,
|
|
29
|
+
// `TouchableOpacity` and `TouchableHighlight` stay COMPONENTS on Vue until their
|
|
30
|
+
// `HOST_PRIMITIVES` entries land — and `intrinsic-elements.ts` already types all three as valid
|
|
31
|
+
// markup off `ISymbioteIntrinsic`, so vue-tsc accepts what the runtime compiler warns about.
|
|
32
|
+
//
|
|
33
|
+
// The tell that the three are still wrapped rather than merely unlisted: `src/register.ts` does
|
|
34
|
+
// NOT call their behaviors, because a registered behavior plus a surviving wrapper is two owners
|
|
35
|
+
// on one node. Those two facts move together or not at all.
|
|
36
|
+
//
|
|
37
|
+
// .cjs, and its own file rather than a copy in each consumer: two transforms carrying one
|
|
38
|
+
// derivation is the drift the shared spec exists to prevent.
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
HOST_PRIMITIVES,
|
|
42
|
+
} = require('@symbiote-native/components/host-primitives');
|
|
43
|
+
|
|
44
|
+
// BOTH tags a spec entry can emit. A primitive with an `intrinsicWhen` selects between two native
|
|
45
|
+
// views by one prop, so its second intrinsic is as hand-writable as its first.
|
|
46
|
+
module.exports = new Set(
|
|
47
|
+
Object.values(HOST_PRIMITIVES).flatMap(entry =>
|
|
48
|
+
entry.intrinsicWhen === undefined
|
|
49
|
+
? [entry.intrinsic]
|
|
50
|
+
: [entry.intrinsic, entry.intrinsicWhen.intrinsic],
|
|
51
|
+
),
|
|
52
|
+
);
|