@symbiote-native/components 2.0.0 → 3.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 +8 -9
- package/build/accessibility-props.d.ts +1 -1
- package/build/accessibility-props.js +2 -2
- package/build/behaviors/activity-indicator/shared.js +41 -70
- package/build/behaviors/button.d.ts +11 -0
- package/build/behaviors/button.js +101 -89
- package/build/behaviors/image-background.d.ts +1 -0
- package/build/behaviors/image-background.js +97 -81
- package/build/behaviors/image.d.ts +0 -1
- package/build/behaviors/image.js +24 -105
- package/build/behaviors/input-accessory-view.d.ts +0 -1
- package/build/behaviors/input-accessory-view.js +47 -53
- package/build/behaviors/pressable.d.ts +1 -1
- package/build/behaviors/pressable.js +68 -97
- package/build/behaviors/refresh-control.js +15 -2
- package/build/behaviors/scroll-view/index.android.js +25 -37
- package/build/behaviors/scroll-view/index.d.ts +1 -0
- package/build/behaviors/scroll-view/index.js +3 -0
- package/build/behaviors/scroll-view/responder.d.ts +4 -0
- package/build/behaviors/scroll-view/responder.js +202 -0
- package/build/behaviors/scroll-view/shared.d.ts +1 -3
- package/build/behaviors/scroll-view/shared.js +95 -95
- package/build/behaviors/scroll-view/sticky.d.ts +1 -0
- package/build/behaviors/scroll-view/sticky.js +62 -49
- package/build/behaviors/switch.js +43 -86
- package/build/behaviors/text-input.js +219 -107
- package/build/behaviors/touchable-highlight.js +74 -61
- package/build/behaviors/touchable-native-feedback.js +43 -122
- package/build/behaviors/touchable-opacity.js +71 -59
- package/build/behaviors/touchable-without-feedback.js +35 -100
- package/build/component-names/index.android.js +7 -11
- package/build/component-names/index.ios.js +0 -7
- package/build/component-names/shared.d.ts +1 -1
- package/build/index.d.ts +15 -21
- package/build/index.js +21 -28
- package/build/resolve-intrinsic.js +3 -9
- package/build/scroll-view-commands.d.ts +1 -9
- package/build/scroll-view-commands.js +13 -74
- package/build/state/flat-list.d.ts +2 -2
- package/build/state/flat-list.js +10 -2
- package/build/state/pressable.d.ts +6 -1
- package/build/state/pressable.js +63 -28
- package/build/state/section-list.d.ts +2 -0
- package/build/state/section-list.js +14 -7
- package/build/state/text-input.d.ts +7 -40
- package/build/state/text-input.js +17 -186
- package/build/state/touchable.d.ts +1 -0
- package/build/state/touchable.js +11 -8
- package/build/state/virtualized-list-reducer.d.ts +2 -2
- package/build/state/virtualized-list.d.ts +6 -6
- package/build/state/virtualized-list.js +71 -37
- package/build/text-props.d.ts +0 -8
- package/build/text-props.js +14 -25
- package/build/view/render-button.d.ts +1 -29
- package/build/view/render-button.js +44 -81
- package/build/view/render-image/index.d.ts +14 -1
- package/build/view/render-image/index.js +22 -147
- package/build/view/render-input-accessory-view.d.ts +1 -5
- package/build/view/render-input-accessory-view.js +26 -48
- package/build/view/render-keyboard-avoiding-view.d.ts +7 -1
- package/build/view/render-keyboard-avoiding-view.js +40 -1
- package/build/view/render-modal.d.ts +1 -1
- package/build/view/render-modal.js +15 -5
- package/build/view/render-pressable/index.d.ts +1 -0
- package/build/view/render-pressable/index.js +4 -0
- package/build/view/render-scroll-view.d.ts +0 -4
- package/build/view/render-scroll-view.js +3 -49
- package/build/view/render-switch.d.ts +0 -14
- package/build/view/render-switch.js +4 -42
- package/build/view/render-touchable-highlight.d.ts +1 -0
- package/build/view/render-touchable-native-feedback.d.ts +0 -1
- package/build/view/render-touchable-native-feedback.js +15 -9
- package/host-primitives.cjs +49 -203
- package/host-primitives.d.cts +0 -1
- package/package.json +3 -7
- package/build/fold-host-bag.d.ts +0 -15
- package/build/fold-host-bag.js +0 -99
- package/build/view/render-text-input.d.ts +0 -11
- package/build/view/render-text-input.js +0 -39
|
@@ -32,15 +32,21 @@ export function selectableBackgroundBorderless(rippleRadius) {
|
|
|
32
32
|
export function rippleBackground(color, borderless, rippleRadius) {
|
|
33
33
|
return { type: 'RippleAndroid', color, borderless, rippleRadius };
|
|
34
34
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// `backgroundProps` WAS HERE AND IS GONE (2026-09-18). It mapped the resolved background plus
|
|
36
|
+
// `useForeground` onto the native slot Android reads, and that is the `#ifdef ANDROID` tail of
|
|
37
|
+
// `foldCloneOntoChild` in `SymbioteFabricProps.cpp` — api-level gate included, since
|
|
38
|
+
// `Platform.Version` on Android IS the api level and `androidApiLevel()` reads the same number.
|
|
39
|
+
//
|
|
40
|
+
// A JS COPY OF A RULE THAT RUNS IN C++, with no runtime caller and no test, reachable only through
|
|
41
|
+
// the package barrel. Found by asking what each `Platform.OS` branch still left in
|
|
42
|
+
// `core/components` is FOR. Its C++ twin is covered on both arms —
|
|
43
|
+
// `android-rules.android.itest.ts` asserts the foreground slot and `clone-onto-child-payload.itest.ts`
|
|
44
|
+
// asserts its absence off Android — and break-testing the gate fires exactly one of them.
|
|
45
|
+
//
|
|
46
|
+
// `canUseNativeForeground` below stays, and the distinction is the reusable half: it is a QUESTION
|
|
47
|
+
// an app asks the platform (`TouchableNativeFeedback.canUseNativeForeground()` is RN's own public
|
|
48
|
+
// static), not a rule that decides a payload. Same class as the slider reading a folded
|
|
49
|
+
// `accessibilityState` — asking is not reimplementing.
|
|
44
50
|
/**
|
|
45
51
|
* RN's four statics, under RN's own spelling — `TouchableNativeFeedback.Ripple(color, borderless)`.
|
|
46
52
|
*
|
package/host-primitives.cjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Vue and Solid apply the same fold in their renderers. Both are correct.
|
|
12
12
|
//
|
|
13
13
|
// Four transforms carried their own copy of this before it existed, and it had already produced a
|
|
14
|
-
// real behaviour split —
|
|
14
|
+
// real behaviour split — the `aliases` note below is what is left of it.
|
|
15
15
|
|
|
16
16
|
// `intrinsicWhen` DECLARED AHEAD OF ITS FIRST ENTRY.
|
|
17
17
|
//
|
|
@@ -35,60 +35,29 @@
|
|
|
35
35
|
// (vitest does not typecheck). The reverse is worse and silent: a field in the `.d.cts` and not
|
|
36
36
|
// here typechecks everywhere and arrives `undefined` in all five transforms. Change both, together.
|
|
37
37
|
/**
|
|
38
|
-
* @typedef {{ op: 'nullish', value: unknown } | { op: 'notFalse' }} IFoldOp
|
|
39
38
|
* @typedef {{ prop: string, intrinsic: string }} IIntrinsicWhen
|
|
40
|
-
* @typedef {{ intrinsic: string,
|
|
39
|
+
* @typedef {{ intrinsic: string, intrinsicWhen?: IIntrinsicWhen }} IHostPrimitive
|
|
41
40
|
*/
|
|
42
41
|
|
|
43
|
-
// `
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// Text.js:222 const _nativeID = id ?? nativeID;
|
|
48
|
-
//
|
|
49
|
-
// Same outcome on both tags: `nativeID = id ?? nativeID`, and the raw `id` key must NOT reach
|
|
50
|
-
// Fabric (no ViewConfig declares it, so it is silently dropped). What the adapters actually did:
|
|
51
|
-
// Solid folded it on both tags, Svelte on View only (its `else` branch skips Text), and Vue on
|
|
52
|
-
// neither — not in either transform, not in its renderer, not in `routeProp`. Vue's gap is OLDER
|
|
53
|
-
// than lowering (its `View` wrapper was always a bare pass-through), so it is a standing
|
|
54
|
-
// <adapters_reach_full_feature_parity> miss, not a lowering regression.
|
|
55
|
-
//
|
|
56
|
-
// WHICH LAYER APPLIES IT IS THE ADAPTER'S CHOICE, exactly as for `defaults` below. Solid and
|
|
57
|
-
// Svelte rename at COMPILE time inside the lowering transform; Vue applies it at RUNTIME
|
|
58
|
-
// (`PROP_ALIASES` in `adapters/vue/src/renderer/index.ts`, in `patchProp`) because Vue has FOUR
|
|
59
|
-
// paths to a node — lowered SFC, lowered TSX, the component wrapper, and a hand-written
|
|
60
|
-
// `h('view', {id})` — and compile time only covers two of them. A transform reading this
|
|
61
|
-
// spec must therefore not assume it owns the fold. Applying it at both layers happens to be
|
|
62
|
-
// harmless here (the rename deletes `id`, so the second pass sees nothing), but that is a property
|
|
63
|
-
// of THIS alias, not a licence.
|
|
64
|
-
//
|
|
65
|
-
// KNOWN DIVERGENCE FROM UPSTREAM, present in two adapters and not introduced by this file: when an
|
|
66
|
-
// element carries BOTH `id` and `nativeID`, RN gives `id` unconditional priority, while a per-key
|
|
67
|
-
// rename (Vue's runtime patchProp, Solid's compile-time rename producing two `nativeID`
|
|
68
|
-
// attributes) lets the LAST one win. Honest parity needs per-node state; no example and no test
|
|
69
|
-
// sets both today.
|
|
70
|
-
const ID_ALIAS = { id: 'nativeID' };
|
|
42
|
+
// `aliases` LEFT THIS SPEC ON 2026-09-18, and what it held was one pair — `id` -> `nativeID` —
|
|
43
|
+
// repeated on all nineteen entries. It is `routeProp`'s now
|
|
44
|
+
// (`core/engine/cpp/tests/js/id-alias-coverage.itest.ts`), which reaches every node rather than
|
|
45
|
+
// every REGISTERED one, and resolves precedence the way upstream does instead of by write order.
|
|
71
46
|
|
|
72
47
|
/** @type {Record<string, IHostPrimitive>} */
|
|
73
48
|
const HOST_PRIMITIVES = {
|
|
74
49
|
View: {
|
|
75
50
|
intrinsic: 'view',
|
|
76
|
-
aliases: ID_ALIAS,
|
|
77
|
-
defaults: {},
|
|
78
51
|
},
|
|
79
52
|
// The five-way switch, thrown 2026-08-23 once all three transforms carried the refusals
|
|
80
53
|
// (`observesState` below). `pressable` resolves to the SAME `RCTView` a plain view does
|
|
81
54
|
// — the tag exists only so the host-behavior registry, which is keyed by TAG and never by
|
|
82
55
|
// resolved name, can find the press machine.
|
|
83
56
|
//
|
|
84
|
-
// No defaults
|
|
85
|
-
//
|
|
86
|
-
// time. (This read "No aliases and no defaults" while the line below already said `ID_ALIAS`, and
|
|
87
|
-
// a Solid test injected a `Pressable` entry with `aliases: {}` on the strength of it.)
|
|
57
|
+
// No defaults: the tag forwards its props untouched, and the machine reads them off `node.props`
|
|
58
|
+
// at event time.
|
|
88
59
|
Pressable: {
|
|
89
60
|
intrinsic: 'pressable',
|
|
90
|
-
aliases: ID_ALIAS,
|
|
91
|
-
defaults: {},
|
|
92
61
|
// Turns on `stateInTemplate` and `renderPropChild`. Without them a render-prop button becomes
|
|
93
62
|
// a tag with no machine — the whole reason this entry landed last.
|
|
94
63
|
observesState: true,
|
|
@@ -102,21 +71,11 @@ const HOST_PRIMITIVES = {
|
|
|
102
71
|
// derives its element set from this table and both Vue compilers read it. Missing here, the tag
|
|
103
72
|
// cost a dev-mode resolve warning per element plus the component codegen path — a slot closure
|
|
104
73
|
// instead of `_createElementBlock`.
|
|
105
|
-
//
|
|
106
|
-
// `aliases: ID_ALIAS` even though each behavior's own `foldPayload` already renames `id`. The
|
|
107
|
-
// two COMPOSE: `foldHostBag` deletes the source key, so the behavior's fold finds no `id` and is
|
|
108
|
-
// a no-op. Measured on the committed payload rather than reasoned, both arms identical — the
|
|
109
|
-
// check `adapter-parity-audit.md` demands before declining a majority value, run in the
|
|
110
|
-
// direction of accepting one.
|
|
111
74
|
TouchableOpacity: {
|
|
112
75
|
intrinsic: 'touchable-opacity',
|
|
113
|
-
aliases: ID_ALIAS,
|
|
114
|
-
defaults: {},
|
|
115
76
|
},
|
|
116
77
|
TouchableHighlight: {
|
|
117
78
|
intrinsic: 'touchable-highlight',
|
|
118
|
-
aliases: ID_ALIAS,
|
|
119
|
-
defaults: {},
|
|
120
79
|
},
|
|
121
80
|
// The second primitive whose TAG depends on a prop, and the first where the prop is one RN's own
|
|
122
81
|
// API takes (`<ScrollView horizontal>`): the axis is a SEPARATE native ViewManager, not a flag on
|
|
@@ -125,8 +84,6 @@ const HOST_PRIMITIVES = {
|
|
|
125
84
|
// Vue's element set — a tag apps write directly and which would otherwise resolve as a component.
|
|
126
85
|
ScrollView: {
|
|
127
86
|
intrinsic: 'scroll-view',
|
|
128
|
-
aliases: ID_ALIAS,
|
|
129
|
-
defaults: {},
|
|
130
87
|
intrinsicWhen: {
|
|
131
88
|
prop: 'horizontal',
|
|
132
89
|
intrinsic: 'horizontal-scroll-view',
|
|
@@ -137,18 +94,10 @@ const HOST_PRIMITIVES = {
|
|
|
137
94
|
// kept because the SEQUENCE is the reusable part — an entry here is a switch for four transforms
|
|
138
95
|
// at once, so it goes in after every side is ready, never to prove the transforms work.
|
|
139
96
|
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// exists there and neither carries a `register.ts` — the same reason they skip Pressable's.
|
|
143
|
-
//
|
|
144
|
-
// 2. The component path no longer shares these tags. It renders `text-input-managed`
|
|
145
|
-
// (`component-names/shared.ts`), because the registry is keyed by TAG and the wrappers run the
|
|
146
|
-
// same machine in their own lifecycle — one shared tag would have installed both copies on a
|
|
147
|
-
// wrapper-built node and fired `setInputFocused` twice per focus.
|
|
97
|
+
// `registerTextInputBehavior()` is called by every adapter's `src/register.ts`: the tag is the
|
|
98
|
+
// only path an app has, so the machine has exactly one owner per node.
|
|
148
99
|
TextInput: {
|
|
149
100
|
intrinsic: 'text-input',
|
|
150
|
-
aliases: ID_ALIAS,
|
|
151
|
-
defaults: {},
|
|
152
101
|
// `multiline` picks between two SEPARATE native views, not one view with a flag, so the tag is
|
|
153
102
|
// decided at compile time and a runtime selector must refuse — a wrong view here is
|
|
154
103
|
// uncorrectable by any later prop write.
|
|
@@ -158,25 +107,18 @@ const HOST_PRIMITIVES = {
|
|
|
158
107
|
},
|
|
159
108
|
},
|
|
160
109
|
// Landed 2026-09-01, same order as TextInput and Image: runtime half built
|
|
161
|
-
// (`core/components/src/behaviors/switch.ts`), registered by
|
|
110
|
+
// (`core/components/src/behaviors/switch.ts`), registered by every adapter, proven
|
|
162
111
|
// against the wrapper's payload (positive + negative controls, a break-tested async-timing case)
|
|
163
112
|
// before this key existed.
|
|
164
113
|
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// node — which already runs that same machine in its own lifecycle — must not also get the
|
|
168
|
-
// engine's copy. `render-switch.ts` emits `switch-managed`; this key's `intrinsic` is
|
|
169
|
-
// the bare tag the behavior registry attaches to.
|
|
114
|
+
// The behavior carries a machine: it mirrors the last value native reported and sends a platform
|
|
115
|
+
// snap-back command on disagreement.
|
|
170
116
|
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// `-managed` split means only the bare `switch` tag ever carries this behavior, and the
|
|
177
|
-
// wrapper never emits that tag, so no node's payload ever passes through this fold more than
|
|
178
|
-
// once. Unreachable by construction, not idempotent by property — the same distinction
|
|
179
|
-
// TextInput's own entry draws for its fold.
|
|
117
|
+
// ITS FOLD IS NOT IDEMPOTENT, and that is safe for a reason Image's entry does not share. Switch
|
|
118
|
+
// maps `trackColor`/`thumbColor`/`ios_backgroundColor` onto native prop names keyed on
|
|
119
|
+
// `Platform.OS`, so running it twice would NOT be a no-op — the question never arises because a
|
|
120
|
+
// payload reaches this fold once, on the one tag that carries the behavior. Unreachable by
|
|
121
|
+
// construction, not idempotent by property.
|
|
180
122
|
//
|
|
181
123
|
// No `observesState`: nothing in Switch's public surface is a function-valued style or a
|
|
182
124
|
// render-prop child (`style?: IStyleProp<IViewStyle>`, never a callback), so neither
|
|
@@ -184,8 +126,6 @@ const HOST_PRIMITIVES = {
|
|
|
184
126
|
// forced that flag.
|
|
185
127
|
Switch: {
|
|
186
128
|
intrinsic: 'switch',
|
|
187
|
-
aliases: ID_ALIAS,
|
|
188
|
-
defaults: {},
|
|
189
129
|
},
|
|
190
130
|
// Filed as NOT LOWERABLE for a week under `.claude/rules/host-primitive-tier.md`'s "SECOND
|
|
191
131
|
// disqualifier" — its own node is a single element, but its POSITION is decided by the ScrollView
|
|
@@ -202,97 +142,59 @@ const HOST_PRIMITIVES = {
|
|
|
202
142
|
// the five wrappers folded exactly `resolveAccessibilityProps`, which the engine already runs at
|
|
203
143
|
// `fabricProps` on every path — the same reason SafeAreaView has no behavior file.
|
|
204
144
|
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// The prop is declared on all five in the same change as this alias — half of it in either
|
|
209
|
-
// direction is broken (a fold for a key nobody can pass, or a raw `id` reaching a view whose
|
|
210
|
-
// ViewConfig declares none).
|
|
211
|
-
//
|
|
212
|
-
// No `-managed` twin: the behavior carries a machine, so it needs one owner per node, and it has
|
|
213
|
-
// one — the wrappers forward to this tag and none of them runs a mirror any more.
|
|
145
|
+
// `id` is declared on all five wrappers, which it was not until 2026-09-01: upstream's
|
|
146
|
+
// RefreshControl spreads `...ViewProps` (RefreshControl.js:70), so its absence was a parity gap
|
|
147
|
+
// rather than a decision. The rename itself is the engine's.
|
|
214
148
|
RefreshControl: {
|
|
215
149
|
intrinsic: 'refresh-control',
|
|
216
|
-
aliases: ID_ALIAS,
|
|
217
150
|
// None. RN seeds nothing: `refreshing` is required, and every other prop is per-platform
|
|
218
151
|
// styling the native view defaults itself.
|
|
219
|
-
defaults: {},
|
|
220
152
|
},
|
|
153
|
+
// `defaults` LEFT THIS SPEC ON 2026-09-18, and Text was the last entry that had any — RN's
|
|
154
|
+
// `ellipsizeMode ?? 'tail'` and `allowFontScaling !== false` (`Text.js:289,291`). The other
|
|
155
|
+
// eighteen entries had declared `{}` for months.
|
|
156
|
+
//
|
|
157
|
+
// It was the THIRD copy of that one rule. `applyTextDefaults` (`core/engine/src/fabric-props.ts`)
|
|
158
|
+
// and its C++ twin apply both to every node committing as `RCTText`, whoever authored it and
|
|
159
|
+
// whatever tag it carries — strictly WIDER than this table, which reached only a registered
|
|
160
|
+
// primitive. Proven redundant rather than argued: emptying it turned ZERO itests red, and the
|
|
161
|
+
// itests are the side that reads a real committed payload.
|
|
221
162
|
Text: {
|
|
222
163
|
intrinsic: 'text',
|
|
223
|
-
aliases: ID_ALIAS,
|
|
224
|
-
// RN's Text.js applies both unconditionally on the non-virtual path. Each key below cites
|
|
225
|
-
// the upstream line verbatim, because THIS DATA is now the thing that must not drift from RN.
|
|
226
|
-
// The
|
|
227
|
-
// authority on what they MEAN is `src/text-props.ts`'s resolveTextProps, which every wrapper
|
|
228
|
-
// path already calls; this is the same fold expressed as data so a COMPILE-time transform can
|
|
229
|
-
// emit it too. `notFalse`, never `nullish` — RN treats an explicit `undefined` like a missing
|
|
230
|
-
// prop and only a literal `false` opts out. Emit both keys unconditionally: a fold whose two
|
|
231
|
-
// branches emit different key sets is the hazard `.claude/rules/solid-descriptor-bridge.md` §1
|
|
232
|
-
// exists for.
|
|
233
|
-
defaults: {
|
|
234
|
-
// Text.js:291 processedProps.ellipsizeMode = ellipsizeMode ?? 'tail';
|
|
235
|
-
ellipsizeMode: { op: 'nullish', value: 'tail' },
|
|
236
|
-
// Text.js:289 processedProps.allowFontScaling = allowFontScaling !== false;
|
|
237
|
-
allowFontScaling: { op: 'notFalse' },
|
|
238
|
-
},
|
|
239
164
|
},
|
|
240
165
|
// FOLD-ONLY: the behavior registered for this tag carries a prop fold and nothing else — no
|
|
241
|
-
// listeners, no commit hook, no per-node runtime (`core/components/src/behaviors/image.ts`).
|
|
242
|
-
// whole of the wrapper's body was prop mapping, so the lowered form owes exactly that.
|
|
166
|
+
// listeners, no commit hook, no per-node runtime (`core/components/src/behaviors/image.ts`).
|
|
243
167
|
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// already emits means a wrapper-built node simply folds a second time to no effect. Asserted in
|
|
247
|
-
// `behaviors/image.test.ts`; break-tested. TextInput's split is NOT about idempotence (its fold
|
|
248
|
-
// is idempotent too) — it is about one owner per node, because that behavior carries a machine.
|
|
168
|
+
// `mapImageProps` is idempotent, asserted in `behaviors/image.test.ts` and break-tested, so a bag
|
|
169
|
+
// that reaches the fold twice folds to no effect.
|
|
249
170
|
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// lowering `Image` at once, so a fold that had not landed would surface as a raw `src` reaching
|
|
253
|
-
// Fabric — a key no ViewConfig declares, which throws nothing and paints nothing.
|
|
171
|
+
// The runtime half has to land before this key does: without the fold a raw `src` reaches Fabric,
|
|
172
|
+
// a key no ViewConfig declares, which throws nothing and paints nothing.
|
|
254
173
|
// THE ENTRY IS NOT OPTIONAL HERE, and the reason has nothing to do with folds: this table is what
|
|
255
174
|
// `adapters/vue/intrinsic-tags.cjs` derives element-vs-component from, and a hyphenated tag it
|
|
256
175
|
// does not name compiles to `resolveComponent("image-background")` — children become a slot the
|
|
257
176
|
// element path never reads, so the subtree renders BLANK with no error. `image`/`view`/`text` are
|
|
258
177
|
// real SVG element names and survive that gap; this one is not.
|
|
259
|
-
//
|
|
260
|
-
// `aliases: ID_ALIAS` was MEASURED against the arm without it rather than reasoned about, because
|
|
261
|
-
// `behaviors/image-background.ts` folds `id` itself on the built image. Both arms commit
|
|
262
|
-
// `nativeID` on the image and no `id` anywhere, and the two compose because an alias DELETES its
|
|
263
|
-
// source key — so the second pass finds nothing. Kept for the property `foldHostBag` provides and
|
|
264
|
-
// the behavior cannot: the rename happens on the OWNER bag, before the redirect, so any adapter
|
|
265
|
-
// path that folds bags gets it whether or not the behavior ever runs.
|
|
266
178
|
ImageBackground: {
|
|
267
179
|
intrinsic: 'image-background',
|
|
268
|
-
aliases: ID_ALIAS,
|
|
269
180
|
// None. The absolute-fill style, the box-dimension proxy and the Image mapping are all derived
|
|
270
181
|
// from live props at commit, which a compile-time seed cannot express.
|
|
271
|
-
defaults: {},
|
|
272
182
|
},
|
|
273
183
|
Image: {
|
|
274
184
|
intrinsic: 'image',
|
|
275
|
-
aliases: ID_ALIAS,
|
|
276
185
|
// None. Every default RN's Image applies is already inside the shared mapping (the source
|
|
277
186
|
// array shape, the width/height style fold, `alt` -> accessibilityLabel), which the behavior
|
|
278
187
|
// runs at commit — so there is nothing left for a compile-time seed to do.
|
|
279
|
-
defaults: {},
|
|
280
188
|
},
|
|
281
|
-
// Entered LAST, same order Image used: runtime half built, registered by the four lowering
|
|
282
|
-
// adapters, and proven against the wrapper's payload before this key existed.
|
|
283
|
-
//
|
|
284
189
|
// The ONLY primitive so far whose intrinsic resolves to a different Fabric component per
|
|
285
190
|
// platform — `RCTInputAccessoryView` on iOS, a plain `RCTView` on Android. The fold is
|
|
286
|
-
// platform-invariant on purpose
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// identical on both paths, and is with the owner as its own decision.
|
|
191
|
+
// platform-invariant on purpose; what it does NOT do is repair what sits underneath, where
|
|
192
|
+
// upstream RN renders nothing at all off iOS. That divergence is with the owner as its own
|
|
193
|
+
// decision.
|
|
290
194
|
InputAccessoryView: {
|
|
291
195
|
intrinsic: 'input-accessory-view',
|
|
292
|
-
aliases: ID_ALIAS,
|
|
293
196
|
// None. The mapping has no aliasing and no derived value — every consumed name leaves under the
|
|
294
197
|
// same name — so there is nothing for a compile-time seed to do.
|
|
295
|
-
defaults: {},
|
|
296
198
|
},
|
|
297
199
|
// The emptiest entry here, and deliberately so — the withholding protocol has nothing to protect
|
|
298
200
|
// for this one. Every other primitive was held back until its runtime half existed and was proven
|
|
@@ -307,35 +209,8 @@ const HOST_PRIMITIVES = {
|
|
|
307
209
|
// `safe-area-view` with children on its framework's own channel (React's third argument,
|
|
308
210
|
// a Vue slot, a Solid JSX child, Angular's `<ng-content>`, a Svelte snippet). That clears the
|
|
309
211
|
// disqualifier in `.claude/rules/host-primitive-tier.md`.
|
|
310
|
-
//
|
|
311
|
-
// NO `ID_ALIAS`, and this is the one place SafeAreaView departs from every entry above it. The
|
|
312
|
-
// alias exists to REPRODUCE a fold the wrapper performs; not one of the five SafeAreaView wrappers
|
|
313
|
-
// folds `id`, and none declares it. Adding the alias here would make the lowered element fold a
|
|
314
|
-
// prop its component spelling passes through untouched — a lowering that ADDS a capability, which
|
|
315
|
-
// `.claude/rules/adapter-parity-audit.md` records as a bug in the same way as one that drops it.
|
|
316
|
-
// That the five entries above all share `ID_ALIAS` is a property of those five primitives, not a
|
|
317
|
-
// house style to copy: the sixth is where "every case so far did X" stops being a rule.
|
|
318
|
-
//
|
|
319
|
-
// The `id` surface gap itself is real and PRE-EXISTING — upstream's SafeAreaView takes `ViewProps`,
|
|
320
|
-
// so RN accepts `id` where our wrappers do not. It predates lowering, is identical on both paths,
|
|
321
|
-
// and closing it means adding `id` to five wrappers AND this alias together, never one of the two.
|
|
322
212
|
SafeAreaView: {
|
|
323
213
|
intrinsic: 'safe-area-view',
|
|
324
|
-
// ID_ALIAS was deliberately ABSENT here until 2026-09-01, because none of the five wrappers
|
|
325
|
-
// declared `id` and aliasing on the lowered path alone would have made lowering ADD a fold the
|
|
326
|
-
// component spelling does not perform. That exposed a real divergence — Solid's renderer folds
|
|
327
|
-
// `id` from two string constants on the write path, so it aliased for a primitive whose spec
|
|
328
|
-
// said not to (`adapters/solid/src/renderer-alias-fold.test.ts`, whose header predicted exactly
|
|
329
|
-
// this the day a primitive stopped sharing the pair).
|
|
330
|
-
//
|
|
331
|
-
// Resolved by closing the gap rather than routing around it: `id` is now declared on all five
|
|
332
|
-
// wrappers and folded here. That keeps Solid's constant-pair fast path (32 001 writes on a
|
|
333
|
-
// benchmark create) and removes a real parity deficit — upstream's SafeAreaView takes the full
|
|
334
|
-
// ViewProps surface, so RN accepts `id` where none of ours did. Half of this is not an option
|
|
335
|
-
// in either direction: the alias without the prop folds a key nobody can pass, and the prop
|
|
336
|
-
// without the alias sends a raw `id` to a view whose ViewConfig declares no such key.
|
|
337
|
-
aliases: ID_ALIAS,
|
|
338
|
-
defaults: {},
|
|
339
214
|
},
|
|
340
215
|
// The one primitive that commits NO NODE: its intrinsic resolves to the engine's anchor, and the
|
|
341
216
|
// behavior (`src/behaviors/touchable-native-feedback.ts`) clones the owner's props onto the single
|
|
@@ -343,22 +218,11 @@ const HOST_PRIMITIVES = {
|
|
|
343
218
|
// that deletes the five wrappers, because the registry is keyed by TAG: a wrapper still emitting
|
|
344
219
|
// its own `pressable` while the behavior is registered would put two press machines on one tree.
|
|
345
220
|
//
|
|
346
|
-
// ID_ALIAS, and it was proposed WITHOUT one on the reasoning that the behavior already reads
|
|
347
|
-
// `id ?? nativeID` itself (:373) so the shared alias would double-fold. Measured instead of
|
|
348
|
-
// reasoned (`behaviors/touchable-native-feedback.test.ts`, "folds `id` the same whichever layer
|
|
349
|
-
// renamed it"): the two compose idempotently — the alias renames on the OWNER, whose props never
|
|
350
|
-
// reach Fabric, and the behavior's `??` then reads the renamed key to the same answer. Declining
|
|
351
|
-
// the pair would have bought nothing and broken Solid's constant-pair fast path, whose guard
|
|
352
|
-
// (`adapters/solid/src/renderer-alias-fold.test.ts`) is what makes one string compare legal on
|
|
353
|
-
// 32 001 prop writes.
|
|
354
|
-
//
|
|
355
221
|
// No `defaults`: RN's TNF seeds nothing at all — every value it derives (`accessible`,
|
|
356
222
|
// `focusable`, `accessibilityState`, the ripple background) depends on ANOTHER prop or on a
|
|
357
223
|
// listener, which is a fold and not a default.
|
|
358
224
|
TouchableNativeFeedback: {
|
|
359
225
|
intrinsic: 'touchable-native-feedback',
|
|
360
|
-
aliases: ID_ALIAS,
|
|
361
|
-
defaults: {},
|
|
362
226
|
},
|
|
363
227
|
// The SECOND primitive that commits no node, same anchor shape and same reason
|
|
364
228
|
// (TouchableWithoutFeedback.js:229,286). Its clone list is not TNF's: the passthrough half is
|
|
@@ -366,16 +230,13 @@ const HOST_PRIMITIVES = {
|
|
|
366
230
|
// drops them — read `src/behaviors/touchable-without-feedback.ts`'s header rather than inheriting
|
|
367
231
|
// the neighbour's fold.
|
|
368
232
|
//
|
|
369
|
-
// ID_ALIAS for the reason measured on TNF: the alias renames on the OWNER, whose props never reach
|
|
370
|
-
// Fabric, and the behavior's own `id ?? nativeID` then reads the renamed key to the same answer.
|
|
371
233
|
// Upstream's passthrough loop lets an explicit `nativeID` win over `id` here (:280-284, unlike
|
|
372
|
-
// TNF's :373); NOT reproduced
|
|
373
|
-
//
|
|
374
|
-
// listener, which is a fold and not a
|
|
234
|
+
// TNF's :373); NOT reproduced — the engine resolves that precedence once, on the way in, and a
|
|
235
|
+
// per-primitive exception to it would be invisible from anywhere the app can see. No `defaults`:
|
|
236
|
+
// every value TWF derives depends on another prop or on a listener, which is a fold and not a
|
|
237
|
+
// default.
|
|
375
238
|
TouchableWithoutFeedback: {
|
|
376
239
|
intrinsic: 'touchable-without-feedback',
|
|
377
|
-
aliases: ID_ALIAS,
|
|
378
|
-
defaults: {},
|
|
379
240
|
},
|
|
380
241
|
// RN's Button is a touchable wrapping a View wrapping a Text and takes NO children — `title` is a
|
|
381
242
|
// string prop (Button.js:363-388) — so the behavior owns the whole subtree and the tag is the
|
|
@@ -383,50 +244,35 @@ const HOST_PRIMITIVES = {
|
|
|
383
244
|
// by TAG, and a wrapper still building its own View/Text under a registered `button` would give
|
|
384
245
|
// every existing Button a second copy of the subtree.
|
|
385
246
|
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
// own `foldPayload` does (it has no spec entry to do it for it), the bare press behavior does not
|
|
390
|
-
// (`Pressable`'s entry does it instead). Measured on the committed payload, no entry here:
|
|
247
|
+
// `id` used to be PLATFORM-DEPENDENT here, and the record is worth keeping because it is what a
|
|
248
|
+
// per-primitive rename costs. Button's touchable is swapped by platform (Button.js:281-284), only
|
|
249
|
+
// the iOS arm renamed `id` itself, and with no entry in this table the committed payload read:
|
|
391
250
|
//
|
|
392
251
|
// iOS nativeID: 'from-id' id: absent the touchable-opacity fold
|
|
393
252
|
// Android nativeID: undefined id: 'from-id' a key no ViewConfig declares -> dropped
|
|
394
253
|
//
|
|
395
|
-
//
|
|
396
|
-
// iOS-side fold exactly as TNF's does: the rename happens on the bag, so `Object.hasOwn(next,
|
|
397
|
-
// 'id')` one layer down finds nothing left to do.
|
|
254
|
+
// A rename that runs once, for every node, on the way in cannot produce that shape at all.
|
|
398
255
|
//
|
|
399
256
|
// No `defaults`: every value RN's Button seeds is derived from another prop or from a listener
|
|
400
257
|
// (`accessible`, `focusable`, the greyed label, the uppercased title), which is a fold, not a
|
|
401
258
|
// default.
|
|
402
259
|
Button: {
|
|
403
260
|
intrinsic: 'button',
|
|
404
|
-
aliases: ID_ALIAS,
|
|
405
|
-
defaults: {},
|
|
406
261
|
},
|
|
407
262
|
// RN wraps the native spinner in a centering `<View>` (ActivityIndicator.js:112), so this tag is
|
|
408
263
|
// that View and the behavior builds `activity-indicator-spinner` under it. Entered in the same
|
|
409
264
|
// commit that deletes the five wrappers: the registry is keyed by TAG, and a wrapper still
|
|
410
265
|
// painting its own spinner while the behavior is registered would give every indicator two.
|
|
411
266
|
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
// iOS spinner: id 'probe' nativeID absent ActivityIndicatorView declares no `id`
|
|
416
|
-
// Android spinner: id 'probe' nativeID absent AndroidProgressBar declares no `id`
|
|
417
|
-
//
|
|
418
|
-
// i.e. identically broken on both, because the platform half of this primitive is the spinner's
|
|
419
|
-
// COLOUR and native extras, and nothing about it touches the name fold. The alias renames on the
|
|
420
|
-
// OWNER's bag, before `slotPropsExcept` routes the survivor down — so the key that reaches the
|
|
421
|
-
// spinner is `nativeID`, which is where RN's `...restProps` puts it too (ActivityIndicator.js:99).
|
|
267
|
+
// `id` renames on the OWNER, before `slotPropsExcept` routes the survivor down — so the key that
|
|
268
|
+
// reaches the spinner is `nativeID`, which is where RN's `...restProps` puts it too
|
|
269
|
+
// (ActivityIndicator.js:99).
|
|
422
270
|
//
|
|
423
271
|
// No `defaults`: `animating` and `hidesWhenStopped` ARE `notFalse` folds, but they belong to the
|
|
424
272
|
// SPINNER, and this table's ops are applied to the tag's own bag before any slot routing. They
|
|
425
273
|
// live in the behavior's spinner fold instead, which is the only layer that can see that node.
|
|
426
274
|
ActivityIndicator: {
|
|
427
275
|
intrinsic: 'activity-indicator',
|
|
428
|
-
aliases: ID_ALIAS,
|
|
429
|
-
defaults: {},
|
|
430
276
|
},
|
|
431
277
|
};
|
|
432
278
|
module.exports = { HOST_PRIMITIVES };
|
package/host-primitives.d.cts
CHANGED
|
@@ -6,7 +6,6 @@ export type IFoldOp = { op: 'nullish'; value: unknown } | { op: 'notFalse' };
|
|
|
6
6
|
|
|
7
7
|
export interface IHostPrimitive {
|
|
8
8
|
intrinsic: string;
|
|
9
|
-
aliases: Readonly<Record<string, string>>;
|
|
10
9
|
defaults: Readonly<Record<string, IFoldOp>>;
|
|
11
10
|
/**
|
|
12
11
|
* The primitive's own state is observable from the TEMPLATE, so a call site that reads it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/components",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Framework-agnostic component logic (state machines + render functions) for SymbioteNative — written once, inherited by every adapter (React, Vue, Angular, ...).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,10 +40,6 @@
|
|
|
40
40
|
"types": "./host-primitives.d.cts",
|
|
41
41
|
"default": "./host-primitives.cjs"
|
|
42
42
|
},
|
|
43
|
-
"./fold-host-bag": {
|
|
44
|
-
"types": "./build/fold-host-bag.d.ts",
|
|
45
|
-
"default": "./build/fold-host-bag.js"
|
|
46
|
-
},
|
|
47
43
|
"./resolve-intrinsic": {
|
|
48
44
|
"types": "./build/resolve-intrinsic.d.ts",
|
|
49
45
|
"default": "./build/resolve-intrinsic.js"
|
|
@@ -63,11 +59,11 @@
|
|
|
63
59
|
},
|
|
64
60
|
"peerDependencies": {
|
|
65
61
|
"react-native": ">=0.86",
|
|
66
|
-
"@symbiote-native/engine": "^0.
|
|
62
|
+
"@symbiote-native/engine": "^1.0.0"
|
|
67
63
|
},
|
|
68
64
|
"devDependencies": {
|
|
69
65
|
"@types/node": "^26.0.0",
|
|
70
|
-
"@symbiote-native/engine": "0.
|
|
66
|
+
"@symbiote-native/engine": "1.0.0"
|
|
71
67
|
},
|
|
72
68
|
"scripts": {
|
|
73
69
|
"typecheck": "tsc --build",
|
package/build/fold-host-bag.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type IFoldOp } from '../host-primitives.cjs';
|
|
2
|
-
export type IHostBag = Record<string, unknown>;
|
|
3
|
-
interface IFoldPlan {
|
|
4
|
-
readonly aliases: ReadonlyArray<readonly [string, string]>;
|
|
5
|
-
readonly defaults: ReadonlyArray<readonly [string, IFoldOp]>;
|
|
6
|
-
}
|
|
7
|
-
export declare const FOLD_PLAN_BY_TAG: ReadonlyMap<string, IFoldPlan>;
|
|
8
|
-
/**
|
|
9
|
-
* Apply a primitive's aliases and defaults to a bag, copy-on-write.
|
|
10
|
-
*
|
|
11
|
-
* Never mutates the input: the bag belongs to whoever built it, and a framework may hand the same
|
|
12
|
-
* object back on a re-render, so writing into it would leak a fold into the author's own state.
|
|
13
|
-
*/
|
|
14
|
-
export declare function foldHostBag(tagName: string, bag: IHostBag): IHostBag;
|
|
15
|
-
export {};
|
package/build/fold-host-bag.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// The RUNTIME half of `HOST_PRIMITIVES` — RN's prop folds applied to a props bag on its way into
|
|
2
|
-
// the engine, keyed by intrinsic tag.
|
|
3
|
-
//
|
|
4
|
-
// Why it exists at all. A primitive's host node is produced by THREE paths, and the compile-time
|
|
5
|
-
// fold in a lowering transform covers exactly one of them:
|
|
6
|
-
//
|
|
7
|
-
// lowered `<View>` the transform folded it covered by the transform
|
|
8
|
-
// `<View>` it refused the wrapper folds it covered by the wrapper
|
|
9
|
-
// a hand-authored tag nobody folds it covered by NOTHING
|
|
10
|
-
//
|
|
11
|
-
// The third path is not hypothetical: Svelte's `components/button.svelte` writes `<text>`
|
|
12
|
-
// directly rather than composing `Text.svelte`, so its title reached Fabric with no
|
|
13
|
-
// `ellipsizeMode` and clipped mid-word where every other adapter ellipsised (2026-08-31). Angular
|
|
14
|
-
// shipped the identical defect from the identical cause, and both fixed it the same way — a seed
|
|
15
|
-
// in the layer every path crosses.
|
|
16
|
-
//
|
|
17
|
-
// Why it is SHARED rather than one copy per adapter: an adapter that lowers has a transform doing
|
|
18
|
-
// this at compile time, an adapter that does not has only this, and a primitive with no wrapper at
|
|
19
|
-
// all (a bare intrinsic tag) has only this everywhere. Three producers of one answer is the shape
|
|
20
|
-
// `<adapters_stay_thin>` exists to stop. Both halves read the same spec, so they cannot drift into
|
|
21
|
-
// two different answers — only into one answer applied twice, which every operation here is
|
|
22
|
-
// idempotent under (`(x ?? 'tail') ?? 'tail'`, `(x !== false) !== false`).
|
|
23
|
-
import { HOST_PRIMITIVES, } from '../host-primitives.cjs';
|
|
24
|
-
const planFor = (primitive) => ({
|
|
25
|
-
aliases: Object.entries(primitive.aliases),
|
|
26
|
-
defaults: Object.entries(primitive.defaults),
|
|
27
|
-
});
|
|
28
|
-
// The wrapper's spelling of a tag whose behavior must attach to the LOWERED path only
|
|
29
|
-
// (`component-names/shared.ts`). A suffix rather than a spec field because it cannot go stale: the
|
|
30
|
-
// next primitive to grow a `-managed` twin is covered the day it is named, with no edit here.
|
|
31
|
-
// `managed-tags-fold.test.ts` pins the convention against the platform tables, so a twin named some
|
|
32
|
-
// other way fails rather than silently losing its folds.
|
|
33
|
-
const managedSpellingOf = (tag) => `${tag}-managed`;
|
|
34
|
-
// EVERY spelling a primitive commits under, and the two axes are independent.
|
|
35
|
-
//
|
|
36
|
-
// `intrinsicWhen` lets one primitive commit two different tags (`TextInput` ->
|
|
37
|
-
// `text-input` / `…-multiline`). `-managed` is the wrapper's twin of each of those. A map
|
|
38
|
-
// keyed on the lowered spellings alone folds the lowered path and silently skips the component one
|
|
39
|
-
// — which is exactly what shipped: TextInput and Switch committed a raw `id`, a key no ViewConfig
|
|
40
|
-
// declares, so Fabric dropped it and the nativeID was lost on device with nothing red. Found by
|
|
41
|
-
// Svelte's equivalence arm, 2026-09-01, and it is the two paths of ONE adapter disagreeing.
|
|
42
|
-
//
|
|
43
|
-
// Folding a bag the wrapper already folded is a no-op — an alias deletes its source key, so the
|
|
44
|
-
// second pass finds nothing to rename — which is what makes covering both paths from one plan safe.
|
|
45
|
-
function tagsOf(primitive) {
|
|
46
|
-
const alternate = primitive.intrinsicWhen?.intrinsic;
|
|
47
|
-
const lowered = alternate === undefined
|
|
48
|
-
? [primitive.intrinsic]
|
|
49
|
-
: [primitive.intrinsic, alternate];
|
|
50
|
-
return [...lowered, ...lowered.map(managedSpellingOf)];
|
|
51
|
-
}
|
|
52
|
-
export const FOLD_PLAN_BY_TAG = new Map(Object.values(HOST_PRIMITIVES).flatMap(primitive => {
|
|
53
|
-
const plan = planFor(primitive);
|
|
54
|
-
return tagsOf(primitive).map((tag) => [tag, plan]);
|
|
55
|
-
}));
|
|
56
|
-
// The runtime twin of a transform's `foldExpression`, which emits these same two operations as
|
|
57
|
-
// SOURCE. `notFalse` is RN's own encoding (`allowFontScaling !== false`): an explicit `undefined`
|
|
58
|
-
// reads as "not set" and only a literal `false` opts out, which is why it is not a `??`.
|
|
59
|
-
function fold(op, authored) {
|
|
60
|
-
return op.op === 'notFalse' ? authored !== false : (authored ?? op.value);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Apply a primitive's aliases and defaults to a bag, copy-on-write.
|
|
64
|
-
*
|
|
65
|
-
* Never mutates the input: the bag belongs to whoever built it, and a framework may hand the same
|
|
66
|
-
* object back on a re-render, so writing into it would leak a fold into the author's own state.
|
|
67
|
-
*/
|
|
68
|
-
export function foldHostBag(tagName, bag) {
|
|
69
|
-
const plan = FOLD_PLAN_BY_TAG.get(tagName);
|
|
70
|
-
if (plan === undefined)
|
|
71
|
-
return bag;
|
|
72
|
-
let next = bag;
|
|
73
|
-
for (const [from, to] of plan.aliases) {
|
|
74
|
-
if (!(from in next))
|
|
75
|
-
continue;
|
|
76
|
-
if (next === bag)
|
|
77
|
-
next = { ...bag };
|
|
78
|
-
// RN gives the alias unconditional priority when both are set (View.js:77-79,
|
|
79
|
-
// `processedProps.nativeID = id`), and the raw key must not survive — no ViewConfig declares
|
|
80
|
-
// `id`, so Fabric would drop it silently.
|
|
81
|
-
next[to] = next[from];
|
|
82
|
-
delete next[from];
|
|
83
|
-
}
|
|
84
|
-
// Seeded whether or not the key was authored: a default that only applies to a key already
|
|
85
|
-
// present is not a default.
|
|
86
|
-
//
|
|
87
|
-
// A bag a transform already folded takes the `continue` on every key and never reaches the copy —
|
|
88
|
-
// which is what keeps the compile-time fold worth having once this one exists. It is no longer
|
|
89
|
-
// the mechanism, but it does keep the hot path allocation-free.
|
|
90
|
-
for (const [key, op] of plan.defaults) {
|
|
91
|
-
const folded = fold(op, next[key]);
|
|
92
|
-
if (key in next && Object.is(next[key], folded))
|
|
93
|
-
continue;
|
|
94
|
-
if (next === bag)
|
|
95
|
-
next = { ...bag };
|
|
96
|
-
next[key] = folded;
|
|
97
|
-
}
|
|
98
|
-
return next;
|
|
99
|
-
}
|