@symbiote-native/vue 2.0.0 → 3.0.1
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 +4 -3
- package/babel-jsx.cjs +20 -11
- package/build/components/flat-list/index.js +1 -1
- package/build/components/keyboard-avoiding-view.js +6 -2
- package/build/components/pressable-props.d.ts +2 -0
- package/build/components/touchable-native-feedback-props.d.ts +1 -0
- package/build/components/touchable-props.d.ts +2 -0
- package/build/components/touchable-without-feedback-props.d.ts +3 -1
- package/build/components/virtualized-list/index.js +24 -11
- package/build/host-instance/index.js +2 -2
- package/build/index.js +1 -1
- package/build/modules/animated/index.d.ts +38 -19
- package/build/renderer/index.js +56 -53
- package/build/runtime-helpers/index.d.ts +1 -1
- package/build/runtime-helpers/index.js +11 -13
- package/build/utils/normalize-attrs.js +3 -3
- package/package.json +5 -5
- package/build/composables/use-raw-attrs.d.ts +0 -1
- package/build/composables/use-raw-attrs.js +0 -26
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ npm install @symbiote-native/vue react-native vue
|
|
|
27
27
|
`react-native` and `vue` stay your app's own top-level dependencies — this package only replaces
|
|
28
28
|
the JS renderer that drives them. **TSX** needs nothing beyond the install above; **SFC**
|
|
29
29
|
additionally needs a Metro transformer for `.vue` files (see
|
|
30
|
-
[`examples/vue-sfc`](../../examples/vue-sfc)'s `metro.config.js`)
|
|
31
|
-
|
|
30
|
+
[`examples/vue-sfc`](../../examples/vue-sfc)'s `metro.config.js`) — `npx @symbiote-native/cli new`
|
|
31
|
+
wires this for a new app; an existing app still needs it copied in by hand.
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
@@ -102,7 +102,8 @@ the demo above is the first one running on the iOS simulator:
|
|
|
102
102
|
## Parity — and the one gap
|
|
103
103
|
|
|
104
104
|
Both adapters reach the same primitives, runtime modules, `Animated` on both drivers, gestures,
|
|
105
|
-
accessibility, and the `VirtualizedList` family
|
|
105
|
+
accessibility, and the `VirtualizedList` family, verified on-device on iOS and Android. That
|
|
106
|
+
parity is **structural, not hand-copied**: the
|
|
106
107
|
component logic (state machines + render functions) is written **once** in `@symbiote-native/components`,
|
|
107
108
|
and each adapter supplies only its lifecycle (Vue's `ref`/`watch` + the descriptor→`h()` bridge).
|
|
108
109
|
|
package/babel-jsx.cjs
CHANGED
|
@@ -11,20 +11,27 @@
|
|
|
11
11
|
// so it has to stay in the app's plugins array, where the app also controls its position relative
|
|
12
12
|
// to its own plugins. A preset would move it after every plugin and silently change that order.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// component that resolves to nothing, with SLOT children an element path
|
|
18
|
-
// never mounts. Blank subtree, no error.
|
|
19
|
-
// isCustomElement only -> nothing was rewritten, so <View> is still a Vue component and the whole
|
|
20
|
-
// point (one component instance per node on ~73% of the tree) is unpaid.
|
|
21
|
-
//
|
|
22
|
-
// Same "both halves or nothing" invariant the SFC path states in metro-vue-transformer.cjs, and the
|
|
23
|
-
// reason it is expressed as one require() here rather than two lines of documentation.
|
|
14
|
+
// `isCustomElement` is the load-bearing option: without it `<view>` compiles to
|
|
15
|
+
// `resolveComponent("view")`, a component that resolves to nothing, with SLOT children an element
|
|
16
|
+
// path never mounts. Blank subtree, no error.
|
|
24
17
|
//
|
|
25
18
|
// @vue/babel-plugin-jsx is OUR dependency and require() resolves relative to this file, so the app
|
|
26
19
|
// declares no extra devDependency — same reasoning as ./metro-css-parser.cjs.
|
|
27
|
-
|
|
20
|
+
//
|
|
21
|
+
// `optimize: true` defaults ON, an app may override it. The plugin's own README calls it
|
|
22
|
+
// experimental ("the optimized code may skip certain re-renders... we strongly recommend thorough
|
|
23
|
+
// testing") because it generates PatchFlags/dynamicProps from JSX's syntax alone, without the
|
|
24
|
+
// template compiler's data-flow analysis. `.vue` SFCs get the equivalent from `@vue/compiler-sfc`
|
|
25
|
+
// unconditionally already (`metro-vue-transformer.cjs`) — this closes the same gap for TSX, which
|
|
26
|
+
// had none. `vue-row-component-shape-cost.itest.ts` measured why it matters: a stateful component
|
|
27
|
+
// re-render pays for `hasPropsChanged`'s full `Object.keys(nextProps)` walk on every patch, even
|
|
28
|
+
// when nothing changed; a patchFlag lets Vue check only the flagged dynamic keys instead.
|
|
29
|
+
// Correctness (a changed prop still recommits, a conditional branch still flips, a keyed list still
|
|
30
|
+
// reorders, and — the shape that actually matters here — a child COMPONENT still re-renders when a
|
|
31
|
+
// nested prop field changes) is pinned in `optimize-flag-safety.test.ts`, including the exact
|
|
32
|
+
// stateful-row shape the itest above measures; that file also confirms a measurable, reproducible
|
|
33
|
+
// JS-side speedup on the identical partial-relabel pattern. NOT verified on-device/Hermes — only
|
|
34
|
+
// headless, through this adapter's real renderer.
|
|
28
35
|
const vueJsx = require('@vue/babel-plugin-jsx');
|
|
29
36
|
|
|
30
37
|
// An intrinsic the renderer resolves through descriptorFor, never a Vue component — including one
|
|
@@ -42,6 +49,8 @@ module.exports = function symbioteVueJsx(options = {}) {
|
|
|
42
49
|
[
|
|
43
50
|
vueJsx,
|
|
44
51
|
{
|
|
52
|
+
// A default an app may override — see the file-level comment above for what backs it.
|
|
53
|
+
optimize: true,
|
|
45
54
|
...options,
|
|
46
55
|
// Last, deliberately: an app may pass other @vue/babel-plugin-jsx options through, but
|
|
47
56
|
// overriding this one silently produces output this adapter cannot render.
|
|
@@ -165,7 +165,7 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots, })
|
|
|
165
165
|
data: rows,
|
|
166
166
|
getItem: (_source, index) => rows[index],
|
|
167
167
|
getItemCount: () => rows.length,
|
|
168
|
-
keyExtractor: rowKeyExtractor,
|
|
168
|
+
keyExtractor: (row) => rowKeyExtractor(row, keyExtractor),
|
|
169
169
|
onEndReached: endReached,
|
|
170
170
|
onStartReached: startReached,
|
|
171
171
|
onRefresh: refresh,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// rawAttrs runs through normalizeVueAttrs so a template `:keyboard-vertical-offset` resolves.
|
|
10
10
|
import { defineComponent, h, ref, onMounted, onUnmounted, } from '@vue/runtime-core';
|
|
11
11
|
import { Keyboard, Platform, dlog, } from '@symbiote-native/engine';
|
|
12
|
-
import { computeInset, keyboardAvoidingEventNamesFor, readKeyboardFrame, readPrefersCrossFadeTransitions, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
|
|
12
|
+
import { computeInset, configureKeyboardAvoidingAnimation, keyboardAvoidingEventNamesFor, readKeyboardAnimationTiming, readKeyboardFrame, readPrefersCrossFadeTransitions, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
|
|
13
13
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
14
14
|
function isStyleProp(value) {
|
|
15
15
|
return typeof value === 'object' && value !== null;
|
|
@@ -53,14 +53,18 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
53
53
|
};
|
|
54
54
|
const onShow = (payload) => {
|
|
55
55
|
const keyboard = readKeyboardFrame(payload);
|
|
56
|
+
const previousInset = inset.value;
|
|
56
57
|
const next = computeInset(frame, keyboard, verticalOffset(), {
|
|
57
58
|
behavior: asBehavior(currentAttrs().behavior),
|
|
58
59
|
// The inset CURRENTLY applied (RN's this.state.bottom), read live off the reactive cell:
|
|
59
60
|
// in 'height' mode the wrapper is shrunk by it, so the next onLayout under-reports the
|
|
60
61
|
// frame by exactly that much and core adds it back.
|
|
61
|
-
previousInset
|
|
62
|
+
previousInset,
|
|
62
63
|
prefersCrossFadeTransitions,
|
|
63
64
|
});
|
|
65
|
+
// RN's `_updateBottomIfNecessary` skips the animation when the inset did not change.
|
|
66
|
+
if (next !== previousInset)
|
|
67
|
+
configureKeyboardAvoidingAnimation(readKeyboardAnimationTiming(payload), currentAttrs().enabled !== false);
|
|
64
68
|
dlog(`KeyboardAvoidingView show -> inset ${next}`);
|
|
65
69
|
inset.value = next;
|
|
66
70
|
};
|
|
@@ -12,7 +12,9 @@ export interface IPressableProps extends IAccessibilityProps, IAriaProps {
|
|
|
12
12
|
disabled?: boolean;
|
|
13
13
|
focusable?: boolean;
|
|
14
14
|
cancelable?: boolean;
|
|
15
|
+
blockNativeResponder?: boolean;
|
|
15
16
|
hitSlop?: IRectOffset;
|
|
17
|
+
testOnly_pressed?: boolean;
|
|
16
18
|
pressRetentionOffset?: IRectOffset;
|
|
17
19
|
unstable_pressDelay?: number;
|
|
18
20
|
minPressDuration?: number;
|
|
@@ -10,6 +10,8 @@ export interface ITouchableOpacityProps extends ITouchableBaseProps {
|
|
|
10
10
|
export interface ITouchableHighlightProps extends ITouchableBaseProps {
|
|
11
11
|
activeOpacity?: number;
|
|
12
12
|
underlayColor?: string;
|
|
13
|
+
touchSoundDisabled?: boolean;
|
|
14
|
+
testOnly_pressed?: boolean;
|
|
13
15
|
onShowUnderlay?: () => void;
|
|
14
16
|
onHideUnderlay?: () => void;
|
|
15
17
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { IPressTimingProps } from '@symbiote-native/components';
|
|
2
2
|
import type { IPressableProps } from './pressable-props';
|
|
3
|
-
export type ITouchableWithoutFeedbackProps = Omit<IPressableProps, 'style'> & IPressTimingProps
|
|
3
|
+
export type ITouchableWithoutFeedbackProps = Omit<IPressableProps, 'style'> & IPressTimingProps & {
|
|
4
|
+
touchSoundDisabled?: boolean;
|
|
5
|
+
};
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// scroll commands would silently no-op. The folded listState is a PLAIN object (not reactive) —
|
|
28
28
|
// mutating it never triggers Vue; the `version` ref is bumped when a transition changes render state.
|
|
29
29
|
import { computed, defineComponent, getCurrentInstance, h, isVNode, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
30
|
-
import {
|
|
30
|
+
import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildScrollViewHandle, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
|
|
31
31
|
import { dlog, isSymbioteNode, } from '@symbiote-native/engine';
|
|
32
32
|
import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
33
33
|
import { componentFromSlot } from '../../utils/slots-to-render-props.js';
|
|
@@ -172,11 +172,15 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
172
172
|
onEndReached: listens('onEndReached')
|
|
173
173
|
? (info) => emit('endReached', info)
|
|
174
174
|
: undefined,
|
|
175
|
-
onEndReachedThreshold:
|
|
175
|
+
onEndReachedThreshold: typeof props.onEndReachedThreshold === 'number'
|
|
176
|
+
? props.onEndReachedThreshold
|
|
177
|
+
: undefined,
|
|
176
178
|
onStartReached: listens('onStartReached')
|
|
177
179
|
? (info) => emit('startReached', info)
|
|
178
180
|
: undefined,
|
|
179
|
-
onStartReachedThreshold:
|
|
181
|
+
onStartReachedThreshold: typeof props.onStartReachedThreshold === 'number'
|
|
182
|
+
? props.onStartReachedThreshold
|
|
183
|
+
: undefined,
|
|
180
184
|
onRefresh: listens('onRefresh')
|
|
181
185
|
? () => emit('refresh')
|
|
182
186
|
: undefined,
|
|
@@ -287,8 +291,12 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
287
291
|
const info = effect.info;
|
|
288
292
|
const map = effect.map;
|
|
289
293
|
const fire = () => {
|
|
290
|
-
for (const pair of pairs)
|
|
291
|
-
pair.onViewableItemsChanged(
|
|
294
|
+
for (const pair of pairs) {
|
|
295
|
+
pair.onViewableItemsChanged({
|
|
296
|
+
...info,
|
|
297
|
+
viewabilityConfig: pair.viewabilityConfig,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
292
300
|
dispatch({ kind: 'viewable-fired', map });
|
|
293
301
|
};
|
|
294
302
|
if (viewableTimer !== null) {
|
|
@@ -452,7 +460,6 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
452
460
|
if (header !== undefined) {
|
|
453
461
|
children.push(h('view', { key: 'list-header' }, [header]));
|
|
454
462
|
}
|
|
455
|
-
let renderedStickyIndices = [];
|
|
456
463
|
if (m.count === FIRST_INDEX) {
|
|
457
464
|
const empty = resolveElement(p.listEmptyComponent);
|
|
458
465
|
if (empty !== undefined) {
|
|
@@ -471,7 +478,7 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
471
478
|
stickyIndices: stickySet,
|
|
472
479
|
hasHeader: header !== undefined,
|
|
473
480
|
});
|
|
474
|
-
|
|
481
|
+
// `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
|
|
475
482
|
if (plan.leadingExtent > EMPTY_OFFSET) {
|
|
476
483
|
children.push(h('view', {
|
|
477
484
|
key: 'spacer-leading',
|
|
@@ -511,7 +518,12 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
511
518
|
cell.index < m.count - 1
|
|
512
519
|
? renderSeparatorElement(p.itemSeparatorComponent, item, p.getItem(p.data, cell.index + 1), separatorOverrides.get(cell.index))
|
|
513
520
|
: undefined;
|
|
514
|
-
children.push(h(
|
|
521
|
+
children.push(h(
|
|
522
|
+
// A cell the app flagged sticky IS the `sticky-header` tag — same position, same key,
|
|
523
|
+
// same `onLayout` (the behavior forwards it rather than replacing it). The collision
|
|
524
|
+
// point then comes from the owner's DOCUMENT order, which is the one form that
|
|
525
|
+
// survives windowing; `stickyHeaderIndices` numbers paint children and does not.
|
|
526
|
+
stickySet?.has(cell.index) === true ? 'sticky-header' : 'view', {
|
|
515
527
|
key: `cell-${cell.key}`,
|
|
516
528
|
onLayout: makeCellMeasure(cell.index),
|
|
517
529
|
style: cellInverted,
|
|
@@ -576,9 +588,10 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
576
588
|
// pre-mount window before the handle attaches).
|
|
577
589
|
if (commandedOffset.value !== undefined)
|
|
578
590
|
scrollProps.contentOffset = commandedOffset.value;
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
591
|
+
// `stickyHeaderIndices` is deliberately NOT forwarded — see `pushCell`. It numbers the scroll
|
|
592
|
+
// view's PAINT children, and a windowed list paints a header, a spacer and a slice, so the
|
|
593
|
+
// positions move every time the window slides and the behavior re-wraps a different child
|
|
594
|
+
// each pass. The cells carry the tag instead.
|
|
582
595
|
// Forward maintainVisibleContentPosition to the ScrollView so it anchors the in-window cells.
|
|
583
596
|
// minIndexForVisible is bumped by 1 when a ListHeaderComponent occupies child 0.
|
|
584
597
|
if (p.maintainVisibleContentPosition !== undefined) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// node in the commit mirror); this only unwraps the Vue-shaped inputs (a Ref or a bare node)
|
|
11
11
|
// onto it. An uncommitted or unknown input has no tag yet and surfaces as null.
|
|
12
12
|
import { isRef, toRaw } from '@vue/runtime-core';
|
|
13
|
-
import { getNativeTag, isSymbioteNode, dlog } from '@symbiote-native/engine';
|
|
13
|
+
import { componentOf, getNativeTag, isSymbioteNode, dlog, } from '@symbiote-native/engine';
|
|
14
14
|
export function findNodeHandle(componentOrHandle) {
|
|
15
15
|
const candidate = isRef(componentOrHandle)
|
|
16
16
|
? componentOrHandle.value
|
|
@@ -24,7 +24,7 @@ export function findNodeHandle(componentOrHandle) {
|
|
|
24
24
|
const node = toRaw(candidate);
|
|
25
25
|
if (isSymbioteNode(node)) {
|
|
26
26
|
const tag = getNativeTag(node) ?? null;
|
|
27
|
-
dlog(`findNodeHandle: component=${node
|
|
27
|
+
dlog(`findNodeHandle: component=${componentOf(node)} tag=${tag}`);
|
|
28
28
|
return tag;
|
|
29
29
|
}
|
|
30
30
|
dlog('findNodeHandle: input did not resolve to a symbiote host node');
|
package/build/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
4
|
// Side-effect import, FIRST and deliberately not a re-export: it installs the engine-side press
|
|
5
|
-
// machine that
|
|
5
|
+
// machine that `<pressable>` needs. `export * from './register'` or a bare
|
|
6
6
|
// import sitting beside a re-export of the same specifier both go lazy under Metro's production
|
|
7
7
|
// `inlineRequires` and the registration silently never runs in a release build. See register.ts.
|
|
8
8
|
import './register.js';
|
|
@@ -5,25 +5,7 @@ export declare const Animated: {
|
|
|
5
5
|
Value: typeof AnimatedValue;
|
|
6
6
|
ValueXY: typeof AnimatedValueXY;
|
|
7
7
|
Color: typeof AnimatedColor;
|
|
8
|
-
Easing:
|
|
9
|
-
step0(n: number): number;
|
|
10
|
-
step1(n: number): number;
|
|
11
|
-
linear(t: number): number;
|
|
12
|
-
ease(t: number): number;
|
|
13
|
-
quad(t: number): number;
|
|
14
|
-
cubic(t: number): number;
|
|
15
|
-
poly(n: number): import("@symbiote-native/engine").IEasingFunction;
|
|
16
|
-
sin(t: number): number;
|
|
17
|
-
circle(t: number): number;
|
|
18
|
-
exp(t: number): number;
|
|
19
|
-
elastic(bounciness?: number): import("@symbiote-native/engine").IEasingFunction;
|
|
20
|
-
back(s?: number): import("@symbiote-native/engine").IEasingFunction;
|
|
21
|
-
bounce(t: number): number;
|
|
22
|
-
bezier(x1: number, y1: number, x2: number, y2: number): import("@symbiote-native/engine").IEasingFunction;
|
|
23
|
-
in(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
|
|
24
|
-
out(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
|
|
25
|
-
inOut(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
|
|
26
|
-
};
|
|
8
|
+
Easing: import("@symbiote-native/engine").IEasing;
|
|
27
9
|
timing: typeof timing;
|
|
28
10
|
spring: typeof spring;
|
|
29
11
|
decay: typeof decay;
|
|
@@ -56,4 +38,41 @@ export declare const Animated: {
|
|
|
56
38
|
FlatList: ReturnType<typeof createAnimatedComponent>;
|
|
57
39
|
SectionList: ReturnType<typeof createAnimatedComponent>;
|
|
58
40
|
createAnimatedComponent: typeof createAnimatedComponent;
|
|
41
|
+
} | {
|
|
42
|
+
Value: typeof AnimatedValue;
|
|
43
|
+
ValueXY: typeof AnimatedValueXY;
|
|
44
|
+
Color: typeof AnimatedColor;
|
|
45
|
+
Easing: import("@symbiote-native/engine").IEasing;
|
|
46
|
+
timing: (value: AnimatedValue, config: import("@symbiote-native/engine").ITimingConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
47
|
+
spring: (value: AnimatedValue, config: import("@symbiote-native/engine").ISpringConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
48
|
+
decay: (_value: AnimatedValue, _config: import("@symbiote-native/engine").IDecayConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
49
|
+
parallel: (animations: import("@symbiote-native/engine").ICompositeAnimation[], _config?: import("@symbiote-native/engine").IParallelConfig) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
50
|
+
sequence: (animations: import("@symbiote-native/engine").ICompositeAnimation[]) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
51
|
+
stagger: (_time: number, animations: import("@symbiote-native/engine").ICompositeAnimation[]) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
52
|
+
loop: (_animation: import("@symbiote-native/engine").ICompositeAnimation) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
53
|
+
delay: (_time: number) => import("@symbiote-native/engine").ICompositeAnimation;
|
|
54
|
+
add: typeof add;
|
|
55
|
+
subtract: typeof subtract;
|
|
56
|
+
multiply: typeof multiply;
|
|
57
|
+
divide: typeof divide;
|
|
58
|
+
modulo: typeof modulo;
|
|
59
|
+
diffClamp: typeof diffClamp;
|
|
60
|
+
event: typeof event;
|
|
61
|
+
forkEvent: typeof forkEvent;
|
|
62
|
+
unforkEvent: typeof unforkEvent;
|
|
63
|
+
View: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
64
|
+
[key: string]: any;
|
|
65
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
66
|
+
Text: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
69
|
+
Image: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
70
|
+
[key: string]: any;
|
|
71
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
72
|
+
ScrollView: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
75
|
+
FlatList: ReturnType<typeof createAnimatedComponent>;
|
|
76
|
+
SectionList: ReturnType<typeof createAnimatedComponent>;
|
|
77
|
+
createAnimatedComponent: typeof createAnimatedComponent;
|
|
59
78
|
};
|
package/build/renderer/index.js
CHANGED
|
@@ -3,41 +3,29 @@
|
|
|
3
3
|
// drives the exact same retained tree React does: the proof the core is framework-
|
|
4
4
|
// agnostic.
|
|
5
5
|
import { callWithErrorHandling, createRenderer, ErrorCodes, markRaw, } from '@vue/runtime-core';
|
|
6
|
-
import { appendChild, createAnchor, createElement, createRawText, dlog, insertBefore, removeChild, routeProp, setProp, setText,
|
|
6
|
+
import { appendChild, createAnchor, createElement, createRawText, childrenOf, dlog, insertBefore, isRawTextNode, isTextContainer, nextSiblingOf, parentOf, removeChild, routeProp, setProp, setText, textOf, toPublicInstance, SymbioteSurface, } from '@symbiote-native/engine';
|
|
7
7
|
import { descriptorFor } from '@symbiote-native/components';
|
|
8
8
|
import { normalizeVueAttrKey } from '../utils/normalize-attrs.js';
|
|
9
9
|
function isSurface(parent) {
|
|
10
10
|
return parent instanceof SymbioteSurface;
|
|
11
11
|
}
|
|
12
12
|
function isRawText(node) {
|
|
13
|
-
return node
|
|
13
|
+
return isRawTextNode(node);
|
|
14
14
|
}
|
|
15
|
-
// RN's Text
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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('view', { id })` no compiler ever sees.
|
|
15
|
+
// RN's two Text defaults left this renderer entirely on 2026-09-18, in two steps a month apart. The
|
|
16
|
+
// CREATE seed went first, because writing them as props cost a crossing every time the app authored
|
|
17
|
+
// the same value — 6 000 per 1 000-row create, measured with `writesOfUnchanged`. What stayed was a
|
|
18
|
+
// clear-back: an explicit `undefined` at patch time was substituted for the default, since RN treats
|
|
19
|
+
// a missing prop and an explicit `undefined` alike.
|
|
35
20
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
21
|
+
// That substitution is gone too, and it was redundant rather than wrong. The rule reads the AUTHORED
|
|
22
|
+
// bag at payload time (`foldTextDefaults`, `SymbioteFabricProps.cpp`), so it cannot tell a cleared
|
|
23
|
+
// prop from one never written — both are absent by the time it looks, and both get the default. The
|
|
24
|
+
// adapter was re-supplying an answer the layer below already had.
|
|
25
|
+
// `PROP_ALIASES` (`id` -> `nativeID`) left this renderer on 2026-09-18 — `routeProp` resolves it
|
|
26
|
+
// for every adapter now, and carries the per-node state the caveat here said was not worth it: with
|
|
27
|
+
// both names on one element upstream gives `id` unconditional priority, where a per-key rename
|
|
28
|
+
// resolved it by write order.
|
|
41
29
|
// RN-style event prop naming ('onPress', 'onValueChange', ...), the same convention JSX itself
|
|
42
30
|
// uses to separate an event from a plain value prop — good enough to decide whether to wrap,
|
|
43
31
|
// without duplicating routeProp's own ViewConfig-derived event lists here.
|
|
@@ -76,12 +64,6 @@ function wrapListenerForErrorHandling(listener, instance) {
|
|
|
76
64
|
// the original's own properties silently breaks that — carry them forward.
|
|
77
65
|
return Object.assign(wrapper, listener);
|
|
78
66
|
}
|
|
79
|
-
// An explicit `undefined` must NOT clear one of those defaults: RN treats a missing prop and an
|
|
80
|
-
// explicit undefined alike, and only a literal `false` opts out of allowFontScaling. Reached
|
|
81
|
-
// only when a value is already undefined, so it costs nothing on the hot path.
|
|
82
|
-
function textDefaultFor(node, key) {
|
|
83
|
-
return node.isText ? TEXT_DEFAULTS.get(key) : undefined;
|
|
84
|
-
}
|
|
85
67
|
// One renderer per mounted surface: the options close over the surface so every mutation
|
|
86
68
|
// can ask it to (microtask-coalesced) recommit. Vue has no resetAfterCommit; instead
|
|
87
69
|
// requestCommit() collapses a burst of insert/patchProp within one tick into a single
|
|
@@ -92,11 +74,9 @@ export function createSymbioteRenderer(surface) {
|
|
|
92
74
|
const descriptor = descriptorFor(type);
|
|
93
75
|
// `type` as the third argument, not just `descriptor.component`: the behavior registry is
|
|
94
76
|
// keyed by the INTRINSIC TAG (`pressable`), while a node only ever carries the
|
|
95
|
-
// resolved Fabric name (`RCTView`). This is the one place that
|
|
96
|
-
//
|
|
77
|
+
// resolved Fabric name (`RCTView`). This is the one place that holds both, so a primitive
|
|
78
|
+
// whose machine lives on the engine node can be matched at all.
|
|
97
79
|
const node = createElement(descriptor.component, descriptor.isText, type);
|
|
98
|
-
if (descriptor.isText)
|
|
99
|
-
seedTextDefaults(node);
|
|
100
80
|
// The imperative public-instance API (measure / setNativeProps / focus / …) is already on
|
|
101
81
|
// the node's prototype, so a template/function ref to a host element exposes it exactly
|
|
102
82
|
// like React's getPublicInstance and toPublicInstance is the identity. The ref must keep
|
|
@@ -145,25 +125,26 @@ export function createSymbioteRenderer(surface) {
|
|
|
145
125
|
// this instead of insert() when an element's children collapse to a single string, so
|
|
146
126
|
// without the check a raw text lands under a non-Text parent - an invalid Fabric tree
|
|
147
127
|
// built silently, which is worse than the throw insert() would have given.
|
|
148
|
-
if (!el
|
|
128
|
+
if (!isTextContainer(el)) {
|
|
149
129
|
throw new Error(`Text string "${text}" must be rendered inside a <Text>`);
|
|
150
130
|
}
|
|
151
131
|
// An RCTText carries its string as a single raw-text child. Reuse a lone existing
|
|
152
132
|
// one to avoid churn; otherwise replace all children with a fresh raw-text node.
|
|
153
|
-
const
|
|
154
|
-
|
|
133
|
+
const existing = childrenOf(el);
|
|
134
|
+
const [first] = existing;
|
|
135
|
+
if (existing.length === 1 && first !== undefined && isRawText(first)) {
|
|
155
136
|
setText(first, text);
|
|
156
137
|
}
|
|
157
138
|
else {
|
|
158
|
-
for (const child of
|
|
139
|
+
for (const child of existing.slice())
|
|
159
140
|
removeChild(el, child);
|
|
160
141
|
appendChild(el, createRawText(text));
|
|
161
142
|
}
|
|
162
143
|
surface.requestCommit();
|
|
163
144
|
},
|
|
164
145
|
insert(child, parent, anchor) {
|
|
165
|
-
if (isRawText(child) && (isSurface(parent) || !parent
|
|
166
|
-
throw new Error(`Text string "${
|
|
146
|
+
if (isRawText(child) && (isSurface(parent) || !isTextContainer(parent))) {
|
|
147
|
+
throw new Error(`Text string "${textOf(child) ?? ''}" must be rendered inside a <Text>`);
|
|
167
148
|
}
|
|
168
149
|
if (isSurface(parent)) {
|
|
169
150
|
if (anchor)
|
|
@@ -182,7 +163,7 @@ export function createSymbioteRenderer(surface) {
|
|
|
182
163
|
remove(child) {
|
|
183
164
|
// A top-level node has no parent (it lives in surface.children); everything else
|
|
184
165
|
// detaches from its retained parent.
|
|
185
|
-
const parent = child
|
|
166
|
+
const parent = parentOf(child);
|
|
186
167
|
if (parent !== undefined)
|
|
187
168
|
removeChild(parent, child);
|
|
188
169
|
else
|
|
@@ -190,23 +171,22 @@ export function createSymbioteRenderer(surface) {
|
|
|
190
171
|
surface.requestCommit();
|
|
191
172
|
},
|
|
192
173
|
parentNode(node) {
|
|
193
|
-
return node
|
|
174
|
+
return parentOf(node) ?? surface;
|
|
194
175
|
},
|
|
195
176
|
nextSibling(node) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return
|
|
177
|
+
// `?? null` because Vue's RendererOptions types the miss as null, not undefined; the
|
|
178
|
+
// engine answers undefined uniformly and the surface fallback lives there now.
|
|
179
|
+
return nextSiblingOf(node, surface) ?? null;
|
|
199
180
|
},
|
|
200
|
-
patchProp(el, key,
|
|
181
|
+
patchProp(el, key, prev, next, _namespace, parentComponent) {
|
|
201
182
|
if (isSurface(el))
|
|
202
183
|
return;
|
|
203
184
|
// Kebab -> camel happens HERE, not only inside a component wrapper: the SFC transformer
|
|
204
185
|
// lowers View/Text to their intrinsic tags (metro-vue-transformer.cjs), so those props
|
|
205
186
|
// arrive one key at a time with no component to fold the bag. Idempotent for the wrapped
|
|
206
187
|
// path, which already normalized.
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const value = next === undefined ? textDefaultFor(el, name) : next;
|
|
188
|
+
const name = normalizeVueAttrKey(key);
|
|
189
|
+
const value = next;
|
|
210
190
|
const routed = EVENT_PROP_NAME.test(name) && typeof value === 'function'
|
|
211
191
|
? wrapListenerForErrorHandling(value, parentComponent ?? null)
|
|
212
192
|
: value;
|
|
@@ -214,7 +194,30 @@ export function createSymbioteRenderer(surface) {
|
|
|
214
194
|
// View becomes a listener; onTintColor on a Switch stays a prop), shared with React. The
|
|
215
195
|
// class/style merge (explicit :style always winning, regardless of which of Vue's two
|
|
216
196
|
// independent patchProp calls lands last) is centralized there too (core/engine/src/node.ts).
|
|
217
|
-
|
|
197
|
+
//
|
|
198
|
+
// `value` REACHES HERE UNCHANGED, on every re-render, and that is upstream by design:
|
|
199
|
+
// `patchProps` excludes it from its own diff and then patches it on its own line
|
|
200
|
+
// (@vue/runtime-core 3.5.39 — `if (next !== prev && key !== "value")`, then
|
|
201
|
+
// `if ("value" in newProps)`). The reason is a DOM one: typing mutates `el.value` directly,
|
|
202
|
+
// so what Vue last SET is not what the element now HOLDS, and only the element can say.
|
|
203
|
+
// Upstream pairs it with a guard in the patcher, and that half we did not have — `patchDOMProp`
|
|
204
|
+
// writes only on a difference against the element's live value (@vue/runtime-dom,
|
|
205
|
+
// `if (oldValue !== newValue || !("_value" in el))`). So every `<text-input>` re-routed its
|
|
206
|
+
// value on every re-render of its parent: 1 000 writes and ~6 000 wire slots per relabel of
|
|
207
|
+
// the benchmark list, against solid's 0 for the identical tree.
|
|
208
|
+
//
|
|
209
|
+
// On `prev` rather than on the node's own prop: the divergence upstream protects against is
|
|
210
|
+
// not visible from here — native text lives on the far side and TextInput's behavior owns the
|
|
211
|
+
// mirror — and asking the host would cost a crossing per input per render to be told the
|
|
212
|
+
// declarative value, which is what `prev` already is.
|
|
213
|
+
//
|
|
214
|
+
// The COMMIT REQUEST still goes out, which is why this skips the route and does not return.
|
|
215
|
+
// The controlled handshake hangs off the commit beat, not off the write: `afterCommit` reads
|
|
216
|
+
// the node's own `value` against its native mirror. An idle commit is O(1) (F-12) and its
|
|
217
|
+
// post-commit hooks run whether or not the commit reached the host (F-7).
|
|
218
|
+
if (key !== 'value' || prev !== next) {
|
|
219
|
+
routeProp(el, name, routed);
|
|
220
|
+
}
|
|
218
221
|
surface.requestCommit();
|
|
219
222
|
},
|
|
220
223
|
// RN has no querySelector / scope-id / innerHTML. The first two are inert; static
|
|
@@ -28,7 +28,7 @@ type IEventHandler<TEvent> = ((event: TEvent, ...args: never[]) => unknown) & {
|
|
|
28
28
|
* (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
|
|
29
29
|
* type there either.
|
|
30
30
|
*
|
|
31
|
-
* `@press.self` on
|
|
31
|
+
* `@press.self` on an element reaches this, and both halves were measured 2026-09-11 rather
|
|
32
32
|
* than assumed — the question arose because a modifier on a COMPONENT takes Vue's own event path
|
|
33
33
|
* and only an element emits the helper. `<pressable @press.self>` compiles to
|
|
34
34
|
* `_withModifiers(fn, ["self"])` imported `from "@symbiote-native/vue/runtime-helpers"` (the Metro
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// real HTMLElement - we have no DOM, so this module supplies our OWN implementation under the
|
|
7
7
|
// same export name instead of leaving `v-show` a silent no-op.
|
|
8
8
|
//
|
|
9
|
-
// Scope: Vue's own template directives (v-show, Teleport) and
|
|
10
|
-
//
|
|
9
|
+
// Scope: Vue's own template directives (v-show, Teleport) and `v-model` on an ELEMENT — see
|
|
10
|
+
// vModelText below for why that is in scope.
|
|
11
11
|
export * from '@vue/runtime-core';
|
|
12
12
|
import { getCurrentInstance, mergeProps as baseMergeProps, normalizeClass, normalizeStyle as baseNormalizeStyle, warn, } from '@vue/runtime-core';
|
|
13
|
-
import { requestCommitFor, setNativeProps, setProp, whenCommitted, } from '@symbiote-native/engine';
|
|
13
|
+
import { componentOf, propOf, requestCommitFor, setNativeProps, setProp, whenCommitted, } from '@symbiote-native/engine';
|
|
14
14
|
import { descriptorFor, SWITCH_TAG } from '@symbiote-native/components';
|
|
15
15
|
// Do NOT shadow runtime-core's own Teleport here, however tempting the guarded ../create-portal
|
|
16
16
|
// wrapper looks (same reasoning that shadowed it in the first place, and it is wrong for this
|
|
@@ -81,12 +81,10 @@ export function normalizeProps(props) {
|
|
|
81
81
|
// contract reads it (`Pressable.js`: `ViewStyleProp | ((state) => ViewStyleProp)` — the callback is
|
|
82
82
|
// top-level, never an array element, so `:style="[a, fn]"` is out of contract here exactly as it is
|
|
83
83
|
// there, and is deliberately NOT rescued).
|
|
84
|
-
// The THIRD door
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// here, and today nothing notices because the lowering transform rewrites that attribute into a
|
|
89
|
-
// resting/active pair before Vue ever emits the helper.
|
|
84
|
+
// The THIRD door. A `:style` binding compiles to `_normalizeStyle(expr)` whenever the compiler
|
|
85
|
+
// cannot keep it on the cheap patch-flag path — an inline arrow and a call expression both do, a
|
|
86
|
+
// bare identifier does not. So `<pressable :style="({pressed}) => …">` would lose its callback here
|
|
87
|
+
// without the rescue below.
|
|
90
88
|
//
|
|
91
89
|
// A TOP-LEVEL function is preserved; one nested inside an array is not, and that asymmetry is the
|
|
92
90
|
// contract rather than an omission. React Native types the prop
|
|
@@ -200,14 +198,14 @@ function modelStateFor(el) {
|
|
|
200
198
|
// by `patchProp` during the same patch, so anything captured earlier is either missing or stale. A
|
|
201
199
|
// prop that is already OUR listener means nothing overwrote it and the app half is unchanged.
|
|
202
200
|
function syncModelListener(el, state) {
|
|
203
|
-
const current = el
|
|
201
|
+
const current = propOf(el, 'onValueChange');
|
|
204
202
|
if (current === state.listener)
|
|
205
203
|
return;
|
|
206
204
|
state.appListener = isValueChangeListener(current) ? current : undefined;
|
|
207
205
|
setProp(el, 'onValueChange', state.listener);
|
|
208
206
|
}
|
|
209
207
|
// Vue's compiler picks the directive by ELEMENT, and for anything it does not recognise as a DOM
|
|
210
|
-
// input it emits `vModelText` —
|
|
208
|
+
// input it emits `vModelText` — `<switch>` included. Stringifying there is what
|
|
211
209
|
// upstream must do (a DOM input's value IS a string) and what we must not: the Switch behavior
|
|
212
210
|
// reads `props.value === true`, so `String(true)` pins the control OFF and no tap can move it.
|
|
213
211
|
// Device-confirmed on `examples/vue-sfc` 2026-09-02, both switches on `CanaryScreen`.
|
|
@@ -217,7 +215,7 @@ function syncModelListener(el, state) {
|
|
|
217
215
|
// of carrying a copy of both.
|
|
218
216
|
const SWITCH_COMPONENT = descriptorFor(SWITCH_TAG).component;
|
|
219
217
|
function isSwitchNode(el) {
|
|
220
|
-
return el
|
|
218
|
+
return componentOf(el) === SWITCH_COMPONENT;
|
|
221
219
|
}
|
|
222
220
|
function syncModelValue(el, value) {
|
|
223
221
|
// The READ half needs no branch: the behavior calls `onValueChange(event)` for both primitives
|
|
@@ -295,7 +293,7 @@ const MODIFIER_GUARDS = {
|
|
|
295
293
|
* (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
|
|
296
294
|
* type there either.
|
|
297
295
|
*
|
|
298
|
-
* `@press.self` on
|
|
296
|
+
* `@press.self` on an element reaches this, and both halves were measured 2026-09-11 rather
|
|
299
297
|
* than assumed — the question arose because a modifier on a COMPONENT takes Vue's own event path
|
|
300
298
|
* and only an element emits the helper. `<pressable @press.self>` compiles to
|
|
301
299
|
* `_withModifiers(fn, ["self"])` imported `from "@symbiote-native/vue/runtime-helpers"` (the Metro
|
|
@@ -12,9 +12,9 @@ const KEBAB_SEGMENT = /-([a-z])/g;
|
|
|
12
12
|
function toCamel(key) {
|
|
13
13
|
return key.replace(KEBAB_SEGMENT, (_match, char) => char.toUpperCase());
|
|
14
14
|
}
|
|
15
|
-
// The per-KEY half, for the path that never sees a whole attrs bag:
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// The per-KEY half, for the path that never sees a whole attrs bag: an intrinsic tag reaches the
|
|
16
|
+
// renderer one patchProp call at a time, with no component in between to fold the bag. Returns the
|
|
17
|
+
// key unchanged (same string identity)
|
|
18
18
|
// whenever there is nothing to convert, which is every key on the hot path.
|
|
19
19
|
export function normalizeVueAttrKey(key) {
|
|
20
20
|
if (!key.includes('-'))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/vue",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "SymbioteNative's Vue adapter — a @vue/runtime-core createRenderer driving real native iOS/Android views through the same engine as the React adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,18 +70,18 @@
|
|
|
70
70
|
"@vue/compiler-sfc": "^3.5.13",
|
|
71
71
|
"@vue/runtime-core": "^3.5.13",
|
|
72
72
|
"typescript": "~6.0.0",
|
|
73
|
-
"@symbiote-native/components": "
|
|
73
|
+
"@symbiote-native/components": "3.0.1",
|
|
74
74
|
"@symbiote-native/css-parser": "0.5.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"vue": ">=3.5.0",
|
|
78
78
|
"react-native": ">=0.86",
|
|
79
|
-
"@symbiote-native/engine": "^
|
|
79
|
+
"@symbiote-native/engine": "^1.2.0"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@babel/core": "^7.25.2",
|
|
83
|
-
"@symbiote-native/engine": "
|
|
84
|
-
"@symbiote-native/test-utils": "0.
|
|
83
|
+
"@symbiote-native/engine": "1.2.0",
|
|
84
|
+
"@symbiote-native/test-utils": "0.4.1"
|
|
85
85
|
},
|
|
86
86
|
"scripts": {
|
|
87
87
|
"typecheck": "tsc --build",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function useRawAttrs(contextAttrs: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// The unproxied twin of a setup context's `attrs`.
|
|
2
|
-
//
|
|
3
|
-
// Vue hands `setupContext.attrs` out as `new Proxy(instance.attrs, attrsProxyHandlers)`, and in
|
|
4
|
-
// PRODUCTION as well as dev that proxy's only trap is `get(target, key) { track(target, "get", "");
|
|
5
|
-
// return target[key]; }` (runtime-core.esm-bundler.js:8357). So every attribute read costs a
|
|
6
|
-
// WeakMap get, a Map get and a `dep.track()`, and the first read of each instance also allocates
|
|
7
|
-
// a Map and a Dep into the module-global `targetMap` that live as long as the component does.
|
|
8
|
-
// Pressable reads ~16 attrs per render and a benchmark create mounts 2 000 of them.
|
|
9
|
-
//
|
|
10
|
-
// `instance.attrs` is the raw object behind that proxy. Capturing it is safe because its identity
|
|
11
|
-
// never changes: `initProps` assigns it once (:4907) and `updateProps` mutates that same object in
|
|
12
|
-
// place (`attrs[key] = value` / `delete attrs[key]`), so a reference taken at setup always reads
|
|
13
|
-
// current values. The tracking the proxy performs is for reads in a setup BODY; a render fn does
|
|
14
|
-
// not need it, because the parent's patch re-runs the child render through `updateComponentPreRender`
|
|
15
|
-
// whether or not the child subscribed.
|
|
16
|
-
//
|
|
17
|
-
// CALL THIS FROM A SETUP BODY ONLY, never from a render fn, a `computed`, or any other deferred
|
|
18
|
-
// callback. `getCurrentInstance()` resolves to `currentInstance || currentRenderingInstance`, so in
|
|
19
|
-
// a callback that runs while a DIFFERENT component is rendering it returns that component and this
|
|
20
|
-
// would silently hand back the wrong bag. `virtualized-list/index.ts` calls normalizeVueAttrs
|
|
21
|
-
// inside a `computed` for exactly that reason and must keep using the context attrs.
|
|
22
|
-
import { getCurrentInstance } from '@vue/runtime-core';
|
|
23
|
-
export function useRawAttrs(contextAttrs) {
|
|
24
|
-
const instance = getCurrentInstance();
|
|
25
|
-
return instance === null ? contextAttrs : instance.attrs;
|
|
26
|
-
}
|