@symbiote-native/vue 0.4.0 → 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/README.md +24 -9
- package/babel-jsx.cjs +53 -0
- package/babel-lower-host-primitives.cjs +225 -0
- package/build/bootstrap.js +1 -1
- package/build/components/button.js +4 -2
- package/build/components/flat-list/index.js +14 -5
- package/build/components/image-background.js +4 -5
- package/build/components/image.js +3 -1
- package/build/components/keyboard-avoiding-view.js +39 -14
- package/build/components/modal/index.js +5 -2
- package/build/components/pressable.d.ts +1 -1
- package/build/components/pressable.js +39 -20
- package/build/components/safe-area-view.d.ts +1 -0
- package/build/components/scroll-view/index.android.d.ts +1 -1
- package/build/components/scroll-view/index.android.js +3 -1
- package/build/components/scroll-view/index.ios.d.ts +1 -1
- package/build/components/scroll-view/index.ios.js +3 -1
- package/build/components/scroll-view/shared.js +33 -13
- package/build/components/scroll-view/sticky-header.js +18 -5
- package/build/components/section-list/index.js +4 -2
- package/build/components/switch/shared.js +12 -3
- package/build/components/text-input/index.js +25 -2
- package/build/components/touchable-native-feedback.js +11 -4
- package/build/components/touchable.d.ts +7 -1
- package/build/components/touchable.js +184 -33
- package/build/components/virtualized-list/index.js +42 -20
- package/build/components/virtualized-section-list/index.d.ts +5 -0
- package/build/components/virtualized-section-list/index.js +33 -4
- package/build/components.d.ts +2 -0
- package/build/components.js +25 -4
- package/build/composables/use-raw-attrs.d.ts +1 -0
- package/build/composables/use-raw-attrs.js +26 -0
- package/build/create-portal/index.d.ts +28 -0
- package/build/create-portal/index.js +42 -0
- package/build/host-instance/index.js +7 -6
- package/build/index.d.ts +9 -6
- package/build/index.js +14 -3
- package/build/modules/animated/create-animated-component.d.ts +1 -1
- package/build/modules/animated/create-animated-component.js +27 -5
- package/build/modules/animated/index.d.ts +2 -0
- package/build/modules/animated/index.js +18 -5
- package/build/modules/app-registry/index.js +4 -2
- package/build/modules/status-bar.js +4 -2
- package/build/register.d.ts +1 -0
- package/build/register.js +35 -0
- package/build/render.d.ts +3 -1
- package/build/render.js +62 -1
- package/build/renderer/index.js +58 -9
- package/build/runtime-helpers/index.d.ts +48 -24
- package/build/runtime-helpers/index.js +314 -30
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +13 -0
- package/build/utils/normalize-attrs.d.ts +1 -0
- package/build/utils/normalize-attrs.js +21 -13
- package/metro-css-parser.cjs +2 -1
- package/metro-vue-transformer.cjs +502 -101
- package/package.json +29 -8
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The Vue adapter's portal — Vue's OWN `<Teleport>`, not a parallel API. Teleport lives in
|
|
2
|
+
// @vue/runtime-core, not @vue/runtime-dom, and resolves its target through the RendererOptions we
|
|
3
|
+
// already supply: `resolveTarget` only consults `querySelector` for the STRING form of `to`, and
|
|
4
|
+
// returns an object target untouched. So stock Teleport drives this renderer as-is — target
|
|
5
|
+
// change, unmount, disabled, call-site provide/inject and reactive updates all measured working
|
|
6
|
+
// over the engine (create-portal.test.ts). This module adds exactly one thing on top: a runtime
|
|
7
|
+
// guard on `to`.
|
|
8
|
+
//
|
|
9
|
+
// Why the guard: `to` is untyped at the template boundary, and a wrong value (a CSS selector
|
|
10
|
+
// copied from web code, a forgotten `.value` on a ref, a plain object) would otherwise sink into
|
|
11
|
+
// insert/remove deep in the engine and corrupt the retained tree silently. Vue's own dev warning
|
|
12
|
+
// for the string form only fires in a dev build and still returns null, which paints nothing.
|
|
13
|
+
//
|
|
14
|
+
// Scope — same-surface targets only, the same PERMANENT boundary as React's createPortal
|
|
15
|
+
// (adapters/react/src/create-portal/index.ts): a portal MOVES host nodes, and only the surface
|
|
16
|
+
// that owns them ever commits them. Content whose target lives in a second, independently
|
|
17
|
+
// mount()ed surface needs createTunnel instead, which copies rather than moves.
|
|
18
|
+
import { defineComponent, h, Teleport as VueTeleport } from '@vue/runtime-core';
|
|
19
|
+
import { isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
20
|
+
export function isTeleportTarget(target) {
|
|
21
|
+
return target instanceof SymbioteSurface || isSymbioteNode(target);
|
|
22
|
+
}
|
|
23
|
+
export const Teleport = defineComponent({
|
|
24
|
+
name: 'Teleport',
|
|
25
|
+
inheritAttrs: false,
|
|
26
|
+
props: {
|
|
27
|
+
to: { type: null, default: null },
|
|
28
|
+
disabled: { type: Boolean, default: false },
|
|
29
|
+
},
|
|
30
|
+
setup(props, { slots }) {
|
|
31
|
+
return () => {
|
|
32
|
+
const { to } = props;
|
|
33
|
+
if (typeof to === 'string') {
|
|
34
|
+
throw new Error(`Teleport target must be a host node ref, not a CSS-selector string ("${to}") — symbiote has no querySelector. Pass a ref to an already-rendered element instead (e.g. <View ref="overlayHost" />, then :to="overlayHost").`);
|
|
35
|
+
}
|
|
36
|
+
if (to != null && !isTeleportTarget(to)) {
|
|
37
|
+
throw new Error('Teleport target is not a real host node — did you forget `.value` on a ref, or pass something symbiote never rendered?');
|
|
38
|
+
}
|
|
39
|
+
return h(VueTeleport, { to, disabled: props.disabled }, slots);
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// findNodeHandle, the Vue adapter twin of adapters/react/src/host-instance.ts. RN's
|
|
2
2
|
// "ref/instance -> native reactTag" lookup, the seam imperative-interop libraries
|
|
3
3
|
// (reanimated, gesture-handler, react-navigation) reach through. A Vue template/function ref
|
|
4
|
-
// to a symbiote host element falls through to the raw engine SymbioteNode,
|
|
5
|
-
//
|
|
6
|
-
// node
|
|
7
|
-
//
|
|
8
|
-
// engine so React and Vue share one definition.
|
|
4
|
+
// to a symbiote host element falls through to the raw engine SymbioteNode, which already carries
|
|
5
|
+
// measure / setNativeProps / focus on its prototype, exactly like React's getPublicInstance.
|
|
6
|
+
// IHostInstance is therefore that node — re-exported from the engine so React and Vue share one
|
|
7
|
+
// definition.
|
|
9
8
|
//
|
|
10
9
|
// The node -> tag resolution itself belongs to the engine (getNativeTag, keyed on the raw
|
|
11
10
|
// node in the commit mirror); this only unwraps the Vue-shaped inputs (a Ref or a bare node)
|
|
@@ -13,7 +12,9 @@
|
|
|
13
12
|
import { isRef, toRaw } from '@vue/runtime-core';
|
|
14
13
|
import { getNativeTag, isSymbioteNode, dlog } from '@symbiote-native/engine';
|
|
15
14
|
export function findNodeHandle(componentOrHandle) {
|
|
16
|
-
const candidate = isRef(componentOrHandle)
|
|
15
|
+
const candidate = isRef(componentOrHandle)
|
|
16
|
+
? componentOrHandle.value
|
|
17
|
+
: componentOrHandle;
|
|
17
18
|
if (candidate === null || candidate === undefined)
|
|
18
19
|
return null;
|
|
19
20
|
if (typeof candidate === 'number')
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import './register';
|
|
2
|
+
export { mount, unmount, setAppConfigurator } from './render';
|
|
3
|
+
export type { IAppConfigurator } from './render';
|
|
4
|
+
export { Teleport, type ITeleportTarget } from './create-portal';
|
|
2
5
|
export { createTunnel, type ITunnel } from './create-tunnel';
|
|
3
6
|
export { View, Text } from './components';
|
|
4
7
|
export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, IResponderProps, } from '@symbiote-native/components';
|
|
@@ -21,7 +24,7 @@ export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground
|
|
|
21
24
|
export { Button } from './components/button';
|
|
22
25
|
export type { IButtonProps } from './components/button';
|
|
23
26
|
export { TextInput } from './components/text-input';
|
|
24
|
-
export type { ITextInputProps, ITextInputHandle } from './components/text-input';
|
|
27
|
+
export type { ITextInputProps, ITextInputHandle, } from './components/text-input';
|
|
25
28
|
export { VirtualizedList } from './components/virtualized-list';
|
|
26
29
|
export type { IVirtualizedListProps, IVirtualizedListSlots, IVirtualizedListEmits, IVirtualizedListHandle, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, ICellLayout, } from './components/virtualized-list';
|
|
27
30
|
export { FlatList } from './components/flat-list';
|
|
@@ -33,7 +36,7 @@ export type { ISection, ISectionListProps, ISectionListSlots, ISectionListEmits,
|
|
|
33
36
|
export { SafeAreaView } from './components/safe-area-view';
|
|
34
37
|
export type { ISafeAreaViewProps } from './components/safe-area-view';
|
|
35
38
|
export { RefreshControl } from './components/refresh-control';
|
|
36
|
-
export type { IRefreshControlProps, IRefreshControlEmits } from './components/refresh-control';
|
|
39
|
+
export type { IRefreshControlProps, IRefreshControlEmits, } from './components/refresh-control';
|
|
37
40
|
export { descriptorToVue } from './descriptor-to-vue';
|
|
38
41
|
export { normalizeVueAttrs } from './utils/normalize-attrs';
|
|
39
42
|
export { resolveModelValue, emitModelUpdate } from './utils/model-binding';
|
|
@@ -56,10 +59,10 @@ export { useWindowDimensions } from './composables/use-window-dimensions';
|
|
|
56
59
|
export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
|
|
57
60
|
export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, IShareContent, IShareOptions, IShareAction, IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, IUrlEvent, II18nManagerConstants, IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, IColorSchemeName, IColorSchemePreference, IAppStateStatus, IAppStateEvent, IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, IBackPressEventName, IBackPressHandler, IPermission, IPermissionStatus, IRationale, IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityEventType, ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, } from '@symbiote-native/engine';
|
|
58
61
|
export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, } from '@symbiote-native/engine';
|
|
59
|
-
export type { ISymbioteEvent, ISymbioteNode, IRootTag } from '@symbiote-native/engine';
|
|
62
|
+
export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
|
|
60
63
|
export type { IViewStyle, ITextStyle, IFlexAlign, IFlexJustify, IPlatformStatic, IPlatformOSType, IPlatformConstantsIOS, IPlatformConstantsAndroid, IPlatformSelectSpec, } from '@symbiote-native/engine';
|
|
61
|
-
export { setNativeViewConfigSource, setDeviceEventSource } from '@symbiote-native/engine';
|
|
62
|
-
export type { INativeViewConfig, INativeViewConfigSource } from '@symbiote-native/engine';
|
|
64
|
+
export { setNativeViewConfigSource, setDeviceEventSource, } from '@symbiote-native/engine';
|
|
65
|
+
export type { INativeViewConfig, INativeViewConfigSource, } from '@symbiote-native/engine';
|
|
63
66
|
export type { IEnterKeyHint, IInputMode, ISubmitBehavior, ITextInputSelection, IImageStatics, } from '@symbiote-native/components';
|
|
64
67
|
export { PixelRatio, PanResponder, PlatformColor, DynamicColorIOS, InteractionManager, } from '@symbiote-native/engine';
|
|
65
68
|
export type { IPixelRatioStatic } from '@symbiote-native/engine';
|
package/build/index.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// @symbiote-native/vue: a thin Vue 3 reconciler over @symbiote-native/engine. createRenderer maps
|
|
2
2
|
// each RendererOptions call onto the engine's mutation API; all Fabric clone-on-write
|
|
3
3
|
// lives in the engine, shared with every other adapter. App code names only @symbiote-native/vue.
|
|
4
|
-
|
|
5
|
-
//
|
|
4
|
+
// Side-effect import, FIRST and deliberately not a re-export: it installs the engine-side press
|
|
5
|
+
// machine that a lowered `<symbiote-pressable>` needs. `export * from './register'` or a bare
|
|
6
|
+
// import sitting beside a re-export of the same specifier both go lazy under Metro's production
|
|
7
|
+
// `inlineRequires` and the registration silently never runs in a release build. See register.ts.
|
|
8
|
+
import './register.js';
|
|
9
|
+
export { mount, unmount, setAppConfigurator } from './render.js';
|
|
10
|
+
// The portal: Vue's own <Teleport>, guarded so `to` must be a node/surface this renderer actually
|
|
11
|
+
// mounted (there is no querySelector). It MOVES host nodes, so it reaches any already-mounted
|
|
12
|
+
// target in the SAME surface — including one you only hold a ref to — and keeps the content's
|
|
13
|
+
// reactive owner at the call site (provide/inject resolve from where it was written). Reaching a
|
|
14
|
+
// second, independently mount()ed surface is a different mechanism: createTunnel, which copies
|
|
15
|
+
// into an <Out/> the destination has to render. See create-portal/index.ts.
|
|
16
|
+
export { Teleport } from './create-portal/index.js';
|
|
6
17
|
export { createTunnel } from './create-tunnel/index.js';
|
|
7
18
|
export { View, Text } from './components.js';
|
|
8
19
|
// IHostInstance is the raw engine node (Vue host refs fall through to it; imperative methods
|
|
@@ -50,7 +61,7 @@ export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, }
|
|
|
50
61
|
// Wired once by the app entry on a real host (like setColorProcessor): hands the engine
|
|
51
62
|
// RN's ViewConfig registry so third-party Fabric views auto-derive their metadata.
|
|
52
63
|
// setDeviceEventSource is the third seam of the same set and travels with them.
|
|
53
|
-
export { setNativeViewConfigSource, setDeviceEventSource } from '@symbiote-native/engine';
|
|
64
|
+
export { setNativeViewConfigSource, setDeviceEventSource, } from '@symbiote-native/engine';
|
|
54
65
|
// Pure utilities that moved to the engine (single source, both adapters re-export):
|
|
55
66
|
// PixelRatio + PanResponder, plus the color builders and the interaction scheduler.
|
|
56
67
|
export { PixelRatio, PanResponder, PlatformColor, DynamicColorIOS, InteractionManager, } from '@symbiote-native/engine';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type Component } from '@vue/runtime-core';
|
|
2
|
-
export declare function createAnimatedComponent(Component: Component): import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
2
|
+
export declare function createAnimatedComponent(Component: Component | string): import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
3
3
|
[key: string]: any;
|
|
4
4
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -12,9 +12,13 @@ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
|
12
12
|
// the COMMITTED props (sticky-header passthrough). Named once here so render/reconcile agree.
|
|
13
13
|
const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
|
|
14
14
|
// Reads either a functional component's or a stateful defineComponent's display name, without a
|
|
15
|
-
// cast, for the wrapper's devtools name.
|
|
15
|
+
// cast, for the wrapper's devtools name. A string base IS its own name — an intrinsic tag carries
|
|
16
|
+
// no displayName to read.
|
|
16
17
|
function baseName(component) {
|
|
17
|
-
if (
|
|
18
|
+
if (typeof component === 'string')
|
|
19
|
+
return component;
|
|
20
|
+
if (component === null ||
|
|
21
|
+
(typeof component !== 'function' && typeof component !== 'object')) {
|
|
18
22
|
return 'Anonymous';
|
|
19
23
|
}
|
|
20
24
|
const display = Reflect.get(component, 'displayName');
|
|
@@ -25,6 +29,8 @@ function baseName(component) {
|
|
|
25
29
|
return name;
|
|
26
30
|
return 'Anonymous';
|
|
27
31
|
}
|
|
32
|
+
// The base may be an intrinsic TAG, not only a component: `Animated.View` is built over `View`,
|
|
33
|
+
// and a primitive that becomes a public tag hands this a string. `h()` takes either.
|
|
28
34
|
export function createAnimatedComponent(Component) {
|
|
29
35
|
return defineComponent({
|
|
30
36
|
name: `Animated(${baseName(Component)})`,
|
|
@@ -68,7 +74,9 @@ export function createAnimatedComponent(Component) {
|
|
|
68
74
|
},
|
|
69
75
|
has: (_target, key) => {
|
|
70
76
|
const instance = instanceRef.value;
|
|
71
|
-
return instance !== null &&
|
|
77
|
+
return (instance !== null &&
|
|
78
|
+
typeof instance === 'object' &&
|
|
79
|
+
Reflect.has(instance, key));
|
|
72
80
|
},
|
|
73
81
|
}));
|
|
74
82
|
return () => {
|
|
@@ -96,10 +104,24 @@ export function createAnimatedComponent(Component) {
|
|
|
96
104
|
const passthroughStyle = readPassthroughStyle(passthrough);
|
|
97
105
|
if (passthroughStyle !== undefined) {
|
|
98
106
|
reduced.style =
|
|
99
|
-
reduced.style === undefined
|
|
107
|
+
reduced.style === undefined
|
|
108
|
+
? passthroughStyle
|
|
109
|
+
: [reduced.style, passthroughStyle];
|
|
100
110
|
}
|
|
101
111
|
reduced.ref = captureRef;
|
|
102
|
-
|
|
112
|
+
// The WHOLE slots object, for a component base AND for a tag base. Not just `default`:
|
|
113
|
+
// the list containers take their content through named scoped slots (`item`,
|
|
114
|
+
// `sectionHeader`, ...), which a default-only forward drops silently — Animated.FlatList
|
|
115
|
+
// would commit empty cells.
|
|
116
|
+
//
|
|
117
|
+
// A string base needs no branch here, and the reason is worth recording because the code
|
|
118
|
+
// that reads it looks like it does. For an element Vue unwraps the object itself, calling
|
|
119
|
+
// `children.default()` and recursing on the RESULT rather than going back through `h`
|
|
120
|
+
// (`normalizeChildren`, the `shapeFlag & (1 | 64)` branch) — so a slot returning a lone
|
|
121
|
+
// VNode would be re-read as an object, found to have no `.default`, and dropped, with no
|
|
122
|
+
// error. That case cannot arise: Vue wraps every slot at initSlots so `slots.default()`
|
|
123
|
+
// returns an ARRAY whatever the author's function returned. Measured both spellings.
|
|
124
|
+
return h(Component, reduced, slots);
|
|
103
125
|
};
|
|
104
126
|
},
|
|
105
127
|
});
|
|
@@ -51,5 +51,7 @@ export declare const Animated: {
|
|
|
51
51
|
[key: string]: any;
|
|
52
52
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
53
53
|
ScrollView: ReturnType<typeof createAnimatedComponent>;
|
|
54
|
+
FlatList: ReturnType<typeof createAnimatedComponent>;
|
|
55
|
+
SectionList: ReturnType<typeof createAnimatedComponent>;
|
|
54
56
|
createAnimatedComponent: typeof createAnimatedComponent;
|
|
55
57
|
};
|
|
@@ -7,6 +7,8 @@ import { AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedMock, Easing, Pl
|
|
|
7
7
|
import { View, Text } from '../../components.js';
|
|
8
8
|
import { Image } from '../../components/image.js';
|
|
9
9
|
import { ScrollView } from '../../components/scroll-view';
|
|
10
|
+
import { FlatList } from '../../components/flat-list/index.js';
|
|
11
|
+
import { SectionList } from '../../components/section-list/index.js';
|
|
10
12
|
import { createAnimatedComponent } from './create-animated-component.js';
|
|
11
13
|
export { createAnimatedComponent } from './create-animated-component.js';
|
|
12
14
|
// View/Text are pure host primitives; Image is the functional renderImage wrapper, and all expose
|
|
@@ -14,12 +16,15 @@ export { createAnimatedComponent } from './create-animated-component.js';
|
|
|
14
16
|
const AnimatedView = createAnimatedComponent(View);
|
|
15
17
|
const AnimatedText = createAnimatedComponent(Text);
|
|
16
18
|
const AnimatedImage = createAnimatedComponent(Image);
|
|
17
|
-
// LAZY, memoized
|
|
18
|
-
// this Animated namespace back
|
|
19
|
-
//
|
|
19
|
+
// LAZY, memoized getters, mirroring RN's `get ScrollView()`. Every scrolling container reaches
|
|
20
|
+
// scroll-view/sticky-header, which imports this Animated namespace back, so a wrap at module
|
|
21
|
+
// scope captures whatever the half-evaluated cycle holds at that instant. Under a bundler's ESM
|
|
22
|
+
// interop that is `undefined`, not a ReferenceError - the wrapper builds fine and then renders
|
|
23
|
+
// nothing. Deferring past module init is what keeps it from firing. FlatList and SectionList
|
|
24
|
+
// reach ScrollView through VirtualizedList, so they sit on the same cycle and take the same shape.
|
|
20
25
|
let animatedScrollView;
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
let animatedFlatList;
|
|
27
|
+
let animatedSectionList;
|
|
23
28
|
// The live, JS-driven driver namespace (real frames). RN's AnimatedImplementation.
|
|
24
29
|
const liveDrivers = {
|
|
25
30
|
Value: AnimatedValue,
|
|
@@ -57,6 +62,14 @@ export const Animated = {
|
|
|
57
62
|
animatedScrollView ??= createAnimatedComponent(ScrollView);
|
|
58
63
|
return animatedScrollView;
|
|
59
64
|
},
|
|
65
|
+
get FlatList() {
|
|
66
|
+
animatedFlatList ??= createAnimatedComponent(FlatList);
|
|
67
|
+
return animatedFlatList;
|
|
68
|
+
},
|
|
69
|
+
get SectionList() {
|
|
70
|
+
animatedSectionList ??= createAnimatedComponent(SectionList);
|
|
71
|
+
return animatedSectionList;
|
|
72
|
+
},
|
|
60
73
|
createAnimatedComponent,
|
|
61
74
|
...drivers,
|
|
62
75
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// only the one Vue-specific seam - building a runnable from a component provider via a synthetic
|
|
6
6
|
// functional root (h() has no createElement-style prop/children spread of its own targets, so
|
|
7
7
|
// the wrap step is a render function rather than an object merge).
|
|
8
|
-
import { h } from '@vue/runtime-core';
|
|
8
|
+
import { h, } from '@vue/runtime-core';
|
|
9
9
|
import { createAppRegistry, dlog, } from '@symbiote-native/engine';
|
|
10
10
|
import { mount } from '../../render.js';
|
|
11
11
|
function runnableFor(componentProvider, getWrapperComponentProvider) {
|
|
@@ -20,7 +20,9 @@ function runnableFor(componentProvider, getWrapperComponentProvider) {
|
|
|
20
20
|
const wrapperComponentProvider = getWrapperComponentProvider();
|
|
21
21
|
const mounted = wrapperComponentProvider === undefined
|
|
22
22
|
? renderRoot
|
|
23
|
-
: () => h(wrapperComponentProvider(appParameters), null, {
|
|
23
|
+
: () => h(wrapperComponentProvider(appParameters), null, {
|
|
24
|
+
default: renderRoot,
|
|
25
|
+
});
|
|
24
26
|
mount(appParameters.rootTag, mounted);
|
|
25
27
|
};
|
|
26
28
|
}
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
// prop change, with the imperative statics attached to the component object.
|
|
7
7
|
//
|
|
8
8
|
// Inputs arrive as attrs (untyped) and run through normalizeVueAttrs before each is narrowed by a guard.
|
|
9
|
-
import { defineComponent, watchEffect } from '@vue/runtime-core';
|
|
9
|
+
import { defineComponent, watchEffect, } from '@vue/runtime-core';
|
|
10
10
|
import { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, isOpaqueColorValue, } from '@symbiote-native/engine';
|
|
11
11
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
12
12
|
function asBoolean(value) {
|
|
13
13
|
return typeof value === 'boolean' ? value : undefined;
|
|
14
14
|
}
|
|
15
15
|
function asBarStyle(value) {
|
|
16
|
-
return value === 'default' ||
|
|
16
|
+
return value === 'default' ||
|
|
17
|
+
value === 'light-content' ||
|
|
18
|
+
value === 'dark-content'
|
|
17
19
|
? value
|
|
18
20
|
: undefined;
|
|
19
21
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Side-effect ONLY. This module exports nothing, on purpose.
|
|
2
|
+
//
|
|
3
|
+
// A lowered `<symbiote-pressable>` is an element, so the press machine lives on the engine node
|
|
4
|
+
// instead of inside a component — and it gets there by `registerPressableBehavior()` having run
|
|
5
|
+
// before the first node of that tag is created. The entry imports this file as a bare
|
|
6
|
+
// `import './register';`.
|
|
7
|
+
//
|
|
8
|
+
// WHY IT CANNOT BE A RE-EXPORT, and why this file has no exports to re-export in the first place.
|
|
9
|
+
// Metro turns on `inlineRequires` for production only: it moves a `require()` down to the first
|
|
10
|
+
// place its binding is USED, and a barrel's `export { X } from './x'` compiles to a lazy getter. A
|
|
11
|
+
// module reached only that way never evaluates unless something names X as a VALUE, so its
|
|
12
|
+
// registration silently never happens — in RELEASE builds only, invisible to tsc, to vitest, and
|
|
13
|
+
// even to grepping the bundle, because the code IS bundled and simply never runs. A bare
|
|
14
|
+
// `import './register';` placed NEXT TO a re-export of the same specifier does not help either:
|
|
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();
|
package/build/render.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type IRootTag, type SymbioteSurface } from '@symbiote-native/engine';
|
|
2
|
-
import type { Component } from '@vue/runtime-core';
|
|
2
|
+
import type { App, Component } from '@vue/runtime-core';
|
|
3
|
+
export type IAppConfigurator = (app: App) => void;
|
|
4
|
+
export declare function setAppConfigurator(configure: IAppConfigurator | undefined): void;
|
|
3
5
|
export declare function mount(rootTag: IRootTag, rootComponent: Component): SymbioteSurface;
|
|
4
6
|
export declare function unmount(rootTag: IRootTag): void;
|
|
5
7
|
declare global {
|
package/build/render.js
CHANGED
|
@@ -1,12 +1,71 @@
|
|
|
1
1
|
// Mount a Vue app onto a Fabric surface. The native host hands us a rootTag; we create
|
|
2
2
|
// a surface for it and let the Vue renderer drive the engine, which commits into
|
|
3
3
|
// nativeFabricUIManager; RN's own renderer never in the path.
|
|
4
|
-
import { createSurface, disposeRoot, dlog, } from '@symbiote-native/engine';
|
|
4
|
+
import { createSurface, disposeRoot, dlog, reportUncaughtError, } from '@symbiote-native/engine';
|
|
5
5
|
import { createSymbioteRenderer } from './renderer/index.js';
|
|
6
6
|
// One Vue app per surface, so a surface can be torn down (unmount) or cleanly
|
|
7
7
|
// re-mounted on the same rootTag: the bridgeless host stops and restarts a surface on
|
|
8
8
|
// Fast Refresh and on lifecycle/focus changes, reusing the rootTag.
|
|
9
9
|
const apps = new Map();
|
|
10
|
+
// A stack this deep already names the screen; the rest is noise in a redbox.
|
|
11
|
+
const MAX_STACK_FRAMES = 20;
|
|
12
|
+
// SFC components carry their name on `__name` (the compiler's), plain ones on `name`; neither is
|
|
13
|
+
// on Vue's public `ConcreteComponent` type, hence the guarded read rather than a cast.
|
|
14
|
+
function displayName(type) {
|
|
15
|
+
for (const key of ['name', '__name']) {
|
|
16
|
+
const value = Reflect.get(type, key);
|
|
17
|
+
if (typeof value === 'string' && value.length > 0)
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return 'Anonymous';
|
|
21
|
+
}
|
|
22
|
+
// Vue's own component trace (getComponentTrace, used by `warn`) is internal, so the frames are
|
|
23
|
+
// walked off the public instance's parent chain. LogBox renders whatever string it is handed;
|
|
24
|
+
// React's shape is `\n in <Name>`, and matching it keeps the redbox reading the same in an
|
|
25
|
+
// app that mixes adapters' errors in one console.
|
|
26
|
+
function componentStack(instance) {
|
|
27
|
+
if (instance === null)
|
|
28
|
+
return null;
|
|
29
|
+
const frames = [];
|
|
30
|
+
let current = instance.$;
|
|
31
|
+
while (current !== null && frames.length < MAX_STACK_FRAMES) {
|
|
32
|
+
frames.push(`\n in ${displayName(current.type)}`);
|
|
33
|
+
current = current.parent;
|
|
34
|
+
}
|
|
35
|
+
return frames.length === 0 ? null : frames.join('');
|
|
36
|
+
}
|
|
37
|
+
// The app-level error seam. Without it, an unclaimed error fell through to Vue's own `logError`
|
|
38
|
+
// (runtime-core errorHandling.ts), which does neither useful thing: in a dev bundle it RE-THROWS
|
|
39
|
+
// out of `app.mount()`, so it escapes this function, aborts the AppRegistry runnable and leaves
|
|
40
|
+
// the surface half-brought-up; in a release bundle it console.errors a bare error with no origin
|
|
41
|
+
// and no component context, never touching `global.ErrorUtils`, so nothing reaches the host.
|
|
42
|
+
// Setting a handler suppresses both branches and routes to the engine's shared channel instead —
|
|
43
|
+
// the native redbox on a device, console.error off one.
|
|
44
|
+
//
|
|
45
|
+
// CAUGHT vs UNCAUGHT — the asymmetry is Vue's own, and it costs us no code. `handleError` walks
|
|
46
|
+
// the `onErrorCaptured` chain BEFORE consulting `config.errorHandler`, and a hook returning
|
|
47
|
+
// `false` stops propagation and returns early (runtime-core 3.5 errorHandling.ts), so a boundary
|
|
48
|
+
// that claims the error never reaches this function at all. That is the behaviour we want and
|
|
49
|
+
// the same split the React adapter makes by hand: writing a boundary IS the developer saying
|
|
50
|
+
// "I am handling this", and answering it with a full-screen redbox over the fallback the app
|
|
51
|
+
// just rendered contradicts what the app asked for.
|
|
52
|
+
//
|
|
53
|
+
// So do NOT "fix" this to report from inside an `onErrorCaptured` of our own — that would
|
|
54
|
+
// re-report exactly the errors a boundary claimed. A hook that returns undefined instead of
|
|
55
|
+
// `false` still lands here, and correctly: Vue treats that error as still propagating.
|
|
56
|
+
function reportToHost(error, instance, info) {
|
|
57
|
+
// `info` is Vue's own phase label ('render function', 'setup function', 'native event
|
|
58
|
+
// handler'), which is the closest thing it has to React's "no error boundary" / "recovered"
|
|
59
|
+
// qualifier. Off a native host it prefixes the log line, where there is no redbox to carry it.
|
|
60
|
+
reportUncaughtError(error, {
|
|
61
|
+
origin: `vue render (${String(info)})`,
|
|
62
|
+
componentStack: componentStack(instance),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
let configurator;
|
|
66
|
+
export function setAppConfigurator(configure) {
|
|
67
|
+
configurator = configure;
|
|
68
|
+
}
|
|
10
69
|
function teardown(rootTag) {
|
|
11
70
|
const app = apps.get(rootTag);
|
|
12
71
|
if (app === undefined)
|
|
@@ -22,6 +81,8 @@ export function mount(rootTag, rootComponent) {
|
|
|
22
81
|
const surface = createSurface(rootTag);
|
|
23
82
|
const renderer = createSymbioteRenderer(surface);
|
|
24
83
|
const app = renderer.createApp(rootComponent);
|
|
84
|
+
app.config.errorHandler = reportToHost;
|
|
85
|
+
configurator?.(app);
|
|
25
86
|
apps.set(rootTag, app);
|
|
26
87
|
// The surface IS the Vue container: a top-level mutation routes to surface.appendChild
|
|
27
88
|
// (renderer.ts), and the engine wraps surface.children in its synthetic flex root.
|
package/build/renderer/index.js
CHANGED
|
@@ -3,14 +3,47 @@
|
|
|
3
3
|
// drives the exact same retained tree React does: the proof the core is framework-
|
|
4
4
|
// agnostic.
|
|
5
5
|
import { createRenderer } from '@vue/runtime-core';
|
|
6
|
-
import { appendChild, createAnchor, createElement, createRawText, dlog, insertBefore, removeChild, routeProp, setText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
|
|
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
|
+
import { normalizeVueAttrKey } from '../utils/normalize-attrs.js';
|
|
8
9
|
function isSurface(parent) {
|
|
9
10
|
return parent instanceof SymbioteSurface;
|
|
10
11
|
}
|
|
11
12
|
function isRawText(node) {
|
|
12
13
|
return node.component === RAW_TEXT_COMPONENT;
|
|
13
14
|
}
|
|
15
|
+
// RN's Text.js applies two defaults on the way to native (core/components/src/text-props.ts:
|
|
16
|
+
// ellipsizeMode 'tail', allowFontScaling true unless literally false). The Vue <Text> wrapper
|
|
17
|
+
// folded them with resolveTextProps; a template that the SFC transformer lowered to the
|
|
18
|
+
// intrinsic `symbiote-text` has no wrapper, so the renderer seeds them instead. Without this a
|
|
19
|
+
// numberOfLines={1} line clips mid-word with no ellipsis — device-observed, and silent.
|
|
20
|
+
const TEXT_DEFAULTS = new Map([
|
|
21
|
+
['ellipsizeMode', 'tail'],
|
|
22
|
+
['allowFontScaling', true],
|
|
23
|
+
]);
|
|
24
|
+
function seedTextDefaults(node) {
|
|
25
|
+
for (const [key, value] of TEXT_DEFAULTS)
|
|
26
|
+
setProp(node, key, value);
|
|
27
|
+
}
|
|
28
|
+
// RN's `id` is the modern W3C-named alias for `nativeID` — View.js copies it over
|
|
29
|
+
// (`processedProps.nativeID = id`), so the two name ONE native prop. React folds it in its
|
|
30
|
+
// component wrapper and Svelte and Solid in their transforms; Vue had it nowhere, so `<View
|
|
31
|
+
// id="x">` reached Fabric with an unknown `id` and no `nativeID`, silently and on device only.
|
|
32
|
+
// It lives in the renderer rather than in a transform because that covers all four Vue paths at
|
|
33
|
+
// once — lowered SFC, lowered TSX, the component wrapper, and a hand-written
|
|
34
|
+
// `h('symbiote-view', { id })` no compiler ever sees.
|
|
35
|
+
//
|
|
36
|
+
// Caveat, and it matches what Solid's compile-time rename already does: with BOTH `id` and
|
|
37
|
+
// `nativeID` on one element the last patchProp wins, where upstream gives `id` priority
|
|
38
|
+
// unconditionally. Honouring that needs per-node state to remember an `id` arrived; no example or
|
|
39
|
+
// test sets both, so the state is not worth carrying.
|
|
40
|
+
const PROP_ALIASES = new Map([['id', 'nativeID']]);
|
|
41
|
+
// An explicit `undefined` must NOT clear one of those defaults: RN treats a missing prop and an
|
|
42
|
+
// explicit undefined alike, and only a literal `false` opts out of allowFontScaling. Reached
|
|
43
|
+
// only when a value is already undefined, so it costs nothing on the hot path.
|
|
44
|
+
function textDefaultFor(node, key) {
|
|
45
|
+
return node.isText ? TEXT_DEFAULTS.get(key) : undefined;
|
|
46
|
+
}
|
|
14
47
|
// One renderer per mounted surface: the options close over the surface so every mutation
|
|
15
48
|
// can ask it to (microtask-coalesced) recommit. Vue has no resetAfterCommit; instead
|
|
16
49
|
// requestCommit() collapses a burst of insert/patchProp within one tick into a single
|
|
@@ -19,13 +52,23 @@ export function createSymbioteRenderer(surface) {
|
|
|
19
52
|
const options = {
|
|
20
53
|
createElement(type) {
|
|
21
54
|
const descriptor = descriptorFor(type);
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
// `type` as the third argument, not just `descriptor.component`: the behavior registry is
|
|
56
|
+
// keyed by the INTRINSIC TAG (`symbiote-pressable`), while a node only ever carries the
|
|
57
|
+
// resolved Fabric name (`RCTView`). This is the one place that still holds both, so a
|
|
58
|
+
// lowered primitive whose machine lives on the engine node can be matched at all.
|
|
59
|
+
const node = createElement(descriptor.component, descriptor.isText, type);
|
|
60
|
+
if (descriptor.isText)
|
|
61
|
+
seedTextDefaults(node);
|
|
62
|
+
// The imperative public-instance API (measure / setNativeProps / focus / …) is already on
|
|
63
|
+
// the node's prototype, so a template/function ref to a host element exposes it exactly
|
|
64
|
+
// like React's getPublicInstance and toPublicInstance is the identity. The ref must keep
|
|
65
|
+
// holding this raw node by identity (shallowRef), never a deep ref — the engine commit
|
|
66
|
+
// mirror is keyed on it.
|
|
67
|
+
//
|
|
68
|
+
// The message is a THUNK, not a template literal: this runs once per node — 9 000 times on
|
|
69
|
+
// one benchmark press — and a literal is built at the call site before dlog can decide
|
|
70
|
+
// anything (see core/engine/src/debug.ts).
|
|
71
|
+
dlog(() => `vue createElement ${descriptor.component} -> public instance`);
|
|
29
72
|
return toPublicInstance(node);
|
|
30
73
|
},
|
|
31
74
|
createText(text) {
|
|
@@ -110,11 +153,17 @@ export function createSymbioteRenderer(surface) {
|
|
|
110
153
|
patchProp(el, key, _prev, next) {
|
|
111
154
|
if (isSurface(el))
|
|
112
155
|
return;
|
|
156
|
+
// Kebab -> camel happens HERE, not only inside a component wrapper: the SFC transformer
|
|
157
|
+
// lowers View/Text to their intrinsic tags (metro-vue-transformer.cjs), so those props
|
|
158
|
+
// arrive one key at a time with no component to fold the bag. Idempotent for the wrapped
|
|
159
|
+
// path, which already normalized.
|
|
160
|
+
const normalized = normalizeVueAttrKey(key);
|
|
161
|
+
const name = PROP_ALIASES.get(normalized) ?? normalized;
|
|
113
162
|
// routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on a
|
|
114
163
|
// View becomes a listener; onTintColor on a Switch stays a prop), shared with React. The
|
|
115
164
|
// class/style merge (explicit :style always winning, regardless of which of Vue's two
|
|
116
165
|
// independent patchProp calls lands last) is centralized there too (core/engine/src/node.ts).
|
|
117
|
-
routeProp(el,
|
|
166
|
+
routeProp(el, name, next === undefined ? textDefaultFor(el, name) : next);
|
|
118
167
|
surface.requestCommit();
|
|
119
168
|
},
|
|
120
169
|
// RN has no querySelector / scope-id / innerHTML. The first two are inert; static
|