@symbiote-native/components 1.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 +11 -13
- package/build/accessibility-props.d.ts +1 -1
- package/build/accessibility-props.js +2 -2
- package/build/behaviors/activity-indicator/index.android.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.android.js +16 -0
- package/build/behaviors/activity-indicator/index.d.ts +3 -0
- package/build/behaviors/activity-indicator/index.ios.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.ios.js +14 -0
- package/build/behaviors/activity-indicator/index.js +5 -0
- package/build/behaviors/activity-indicator/shared.d.ts +18 -0
- package/build/behaviors/activity-indicator/shared.js +120 -0
- package/build/behaviors/button.d.ts +13 -0
- package/build/behaviors/button.js +340 -0
- package/build/behaviors/image-background.d.ts +3 -0
- package/build/behaviors/image-background.js +155 -0
- package/build/behaviors/image.d.ts +1 -2
- package/build/behaviors/image.js +25 -106
- package/build/behaviors/input-accessory-view.d.ts +1 -2
- package/build/behaviors/input-accessory-view.js +49 -55
- package/build/behaviors/pressable.d.ts +59 -1
- package/build/behaviors/pressable.js +142 -96
- package/build/behaviors/refresh-control.d.ts +2 -0
- package/build/behaviors/refresh-control.js +96 -0
- package/build/behaviors/scroll-view/index.android.d.ts +1 -0
- package/build/behaviors/scroll-view/index.android.js +40 -0
- package/build/behaviors/scroll-view/index.d.ts +3 -0
- package/build/behaviors/scroll-view/index.ios.d.ts +1 -0
- package/build/behaviors/scroll-view/index.ios.js +10 -0
- package/build/behaviors/scroll-view/index.js +8 -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 +9 -0
- package/build/behaviors/scroll-view/shared.js +291 -0
- package/build/behaviors/scroll-view/sticky.d.ts +18 -0
- package/build/behaviors/scroll-view/sticky.js +581 -0
- package/build/behaviors/switch.d.ts +1 -1
- package/build/behaviors/switch.js +49 -88
- package/build/behaviors/text-input.d.ts +2 -2
- package/build/behaviors/text-input.js +244 -104
- package/build/behaviors/touchable-highlight.d.ts +9 -0
- package/build/behaviors/touchable-highlight.js +205 -0
- package/build/behaviors/touchable-native-feedback.d.ts +20 -0
- package/build/behaviors/touchable-native-feedback.js +254 -0
- package/build/behaviors/touchable-opacity.d.ts +12 -0
- package/build/behaviors/touchable-opacity.js +239 -0
- package/build/behaviors/touchable-without-feedback.d.ts +2 -0
- package/build/behaviors/touchable-without-feedback.js +231 -0
- package/build/component-names/index.android.js +31 -25
- package/build/component-names/index.ios.js +28 -23
- package/build/component-names/shared.d.ts +2 -1
- package/build/component-names/shared.js +16 -6
- package/build/descriptor.js +4 -4
- package/build/index.d.ts +26 -26
- package/build/index.js +56 -27
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/resolve-intrinsic.js +3 -9
- package/build/scroll-view-commands.d.ts +1 -5
- package/build/scroll-view-commands.js +23 -85
- 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 +10 -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 +11 -4
- package/build/view/render-button.js +74 -22
- 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 +18 -8
- package/build/view/render-pressable/index.d.ts +3 -0
- package/build/view/render-pressable/index.js +28 -0
- package/build/view/render-scroll-view.d.ts +1 -4
- package/build/view/render-scroll-view.js +17 -54
- package/build/view/render-switch.d.ts +4 -15
- 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 +19 -1
- package/build/view/render-touchable-native-feedback.js +34 -9
- package/host-primitives.cjs +178 -280
- package/host-primitives.d.cts +0 -3
- package/package.json +8 -21
- package/build/fold-host-bag.d.ts +0 -15
- package/build/fold-host-bag.js +0 -99
- package/build/state-style.d.ts +0 -15
- package/build/state-style.js +0 -47
- package/build/view/render-activity-indicator.d.ts +0 -25
- package/build/view/render-activity-indicator.js +0 -88
- package/build/view/render-image-background.d.ts +0 -9
- package/build/view/render-image-background.js +0 -48
- package/build/view/render-text-input.d.ts +0 -11
- package/build/view/render-text-input.js +0 -39
- package/lowering-fixtures.cjs +0 -259
- package/lowering-fixtures.d.cts +0 -17
- package/specialize-state-style.cjs +0 -219
- package/specialize-state-style.d.cts +0 -15
package/lowering-fixtures.cjs
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// The shared VERDICT table for host-primitive lowering — the fifth parity surface named in
|
|
2
|
-
// `.claude/rules/adapter-parity-audit.md`.
|
|
3
|
-
//
|
|
4
|
-
// WHY A TABLE OF CASES AND NOT SHARED CODE. Four transforms implement one rule set —
|
|
5
|
-
// `adapters/solid/babel-lower-host-primitives.cjs`, `adapters/vue/babel-lower-host-primitives.cjs`,
|
|
6
|
-
// `adapters/vue/metro-vue-transformer.cjs`, `adapters/svelte/src/preprocessor/lower-host-
|
|
7
|
-
// primitives.ts` — over three different plumbings: a Babel plugin holding a real AST, an SFC
|
|
8
|
-
// transform handed SOURCE TEXT, and a Svelte preprocessor that reads ESTree and emits text. They
|
|
9
|
-
// share a spec (`host-primitives.cjs`) and a specialiser (`specialize-state-style.cjs`), and
|
|
10
|
-
// sharing those proves NOTHING about the answer each one gives — which is the whole gap. So what is
|
|
11
|
-
// shared here is the QUESTION and the EXPECTED ANSWER; the snippet that asks it stays per-framework,
|
|
12
|
-
// because `<View {...spread}>` and `<View v-bind="x">` are the same case in two syntaxes.
|
|
13
|
-
//
|
|
14
|
-
// TWO LEVELS, ONE VERDICT. Coverage is decided by INVOCATION — the style callback is called once
|
|
15
|
-
// per state and both results ride the bag as `style` + `activeStyle`, which is the same answer on
|
|
16
|
-
// all five adapters. Compile-time SUBSTITUTION (`specialize-state-style.cjs`) is an optimisation a
|
|
17
|
-
// transform applies when it can prove the body, saving a closure allocation; it never changes a
|
|
18
|
-
// verdict. So every row below is an equality across adapters even though what each one costs
|
|
19
|
-
// differs. A transform reporting `refuse` on a row the specialiser cannot prove has wired
|
|
20
|
-
// substitution as the MECHANISM rather than as the optimisation, and that is the drift this table
|
|
21
|
-
// exists to catch.
|
|
22
|
-
//
|
|
23
|
-
// A REFUSAL CATEGORY CAN BELONG TO THE TRANSFORM RATHER THAN TO THE LANGUAGE, and
|
|
24
|
-
// `REFUSAL_CATEGORIES.emitStyleExpressionOnce` is the worked example — and it carries that name
|
|
25
|
-
// because it was FIRST written as a refusal called `unrepeatableRead`, then corrected to a
|
|
26
|
-
// requirement on the output. It names `style={getStyle()}`,
|
|
27
|
-
// `style={bag[i]}`, `style={flag ? a : b}` — expressions that change meaning when read twice. But
|
|
28
|
-
// the pair is only built by reading TWICE if the transform prints the expression twice, which an
|
|
29
|
-
// inline guard (`typeof f === 'function' ? f({pressed}) : f`) does and a runtime helper
|
|
30
|
-
// (`resolveStateStyle(expr)`) does not: the helper reads the expression ONCE and calls its RESULT
|
|
31
|
-
// twice. Measured 2026-08-23 — Svelte lowers all three with exactly one read in the emitted text;
|
|
32
|
-
// Vue refused them while emitting the guard inline.
|
|
33
|
-
//
|
|
34
|
-
// So a row asserting `refuse` there would encode one transform's emit shape as a shared law and
|
|
35
|
-
// cost every other transform real coverage. Rows for these shapes belong in the table as `lower`,
|
|
36
|
-
// with "emit the expression once" stated as the requirement — which makes a double-reading
|
|
37
|
-
// transform FAIL the table instead of being ratified by it. The general form: before adding a
|
|
38
|
-
// refusal row, ask whether the hazard survives a different emit. If it does not, the row belongs
|
|
39
|
-
// to the emit.
|
|
40
|
-
//
|
|
41
|
-
// THE CONTRACT INVOCATION IMPOSES, stated because it is now observable: a style callback must be
|
|
42
|
-
// PURE in `pressed`. It is executed twice. A side-effecting body was already broken under
|
|
43
|
-
// substitution, but only invocation runs it.
|
|
44
|
-
//
|
|
45
|
-
// A `refuse` ROW IS UNPROVEN UNTIL A CONTROL ON THE SAME PRIMITIVE GOES THE OTHER WAY. `refuse` is
|
|
46
|
-
// the ABSENCE of an observation, and "no intrinsic in the output" is produced equally by a
|
|
47
|
-
// transform that refused and by a primitive nothing can lower — an unimported component, a
|
|
48
|
-
// hardcoded default element, an entry withheld from the spec while its runtime half is wired.
|
|
49
|
-
// Withholding is deliberate practice here, so the ambiguous state recurs by design. Measured
|
|
50
|
-
// 2026-08-31: the `TextInput` entry was withdrawn and both `intrinsic-choice-*` rows went GREEN on
|
|
51
|
-
// all three runners, in three different mechanisms. So a runner asserts a shape that MUST lower
|
|
52
|
-
// before it reads a refusal — and the control's failure message says the row cannot distinguish
|
|
53
|
-
// the two, which is the true state. Full account: `.claude/rules/adapter-parity-audit.md`.
|
|
54
|
-
//
|
|
55
|
-
// WHY IT MATTERS THAT NO OTHER AUDIT SEES THIS. Barrels, subpaths, `files` coverage and exported
|
|
56
|
-
// symbols are all untouched by a transform that lowers a call site its sibling refuses. Every suite
|
|
57
|
-
// stays green and the divergence surfaces either as one adapter being mysteriously slower, or as a
|
|
58
|
-
// button that lowered when it should not have and therefore does not respond.
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {'lower' | 'refuse'} IVerdict
|
|
62
|
-
* @typedef {{ id: string, what: string, expected: IVerdict, why: string }} ILoweringCase
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Every case a lowering transform must answer the same way. Each adapter's test supplies its own
|
|
67
|
-
* snippet per `id` and asserts `expected`; a case with no snippet is itself a failure, so adding a
|
|
68
|
-
* row here forces every transform to declare where it stands.
|
|
69
|
-
* @type {ReadonlyArray<ILoweringCase>}
|
|
70
|
-
*/
|
|
71
|
-
const LOWERING_CASES = [
|
|
72
|
-
{
|
|
73
|
-
id: 'inert-object-style',
|
|
74
|
-
what: 'style is an object literal',
|
|
75
|
-
expected: 'lower',
|
|
76
|
-
why: 'provably not a function, so the template cannot be reading press state through it',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'hoisted-identifier-style',
|
|
80
|
-
what: 'style is a bare identifier that may hold an object OR a function',
|
|
81
|
-
expected: 'lower',
|
|
82
|
-
why: "the compile-time allow-list could never decide this one, which is why it used to refuse. INVOCATION decides it at runtime instead — `typeof f === 'function' ? f({pressed}) : f` — so the shape that no substitution can prove becomes the shape that needs no proof",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 'specialisable-state-style',
|
|
86
|
-
what: 'style is an arrow taking { pressed } and returning one object literal',
|
|
87
|
-
expected: 'lower',
|
|
88
|
-
why: 'INVOCATION covers it — the callback is called once per state and both results ride the bag as style + activeStyle. `specialize-state-style.cjs` is an OPTIMISATION on top for the bodies a transform can prove, saving the closure; it never changes the verdict',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: 'nested-function-state-style',
|
|
92
|
-
what: 'the state style body contains another function',
|
|
93
|
-
expected: 'lower',
|
|
94
|
-
why: 'the SPECIALISER refuses this body — it cannot prove a nested function — but the verdict is set by invocation, which does not need to prove anything. The case stays in the table precisely because it separates the two levels: a transform that reports `refuse` here has wired substitution as the mechanism instead of as the optimisation',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'call-expression-style',
|
|
98
|
-
what: 'style is a call expression',
|
|
99
|
-
expected: 'lower',
|
|
100
|
-
why: "`REFUSAL_CATEGORIES.emitStyleExpressionOnce` — safe exactly when the transform prints the expression ONCE and calls the RESULT twice. A transform printing an inline guard repeats it and runs the author's call once per copy per recompute; that is its emit to fix, not a shape to ban. Assert `occurrences(out, expr) === 1` on the output",
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 'computed-member-style',
|
|
104
|
-
what: 'style is a computed member expression',
|
|
105
|
-
expected: 'lower',
|
|
106
|
-
why: 'same requirement as the call expression, and the index is the tell: printed twice, `bag[i]` is evaluated twice',
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: 'conditional-style',
|
|
110
|
-
what: 'style is a conditional expression',
|
|
111
|
-
expected: 'lower',
|
|
112
|
-
why: 'same requirement, and the sharpest failure of breaking it: printed twice, the two reads are free to take DIFFERENT branches, so the resting and pressed halves come from unrelated objects',
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
id: 'zero-arity-child',
|
|
116
|
-
what: 'children take no parameter',
|
|
117
|
-
expected: 'lower',
|
|
118
|
-
why: 'an ordinary lazy child, not a render prop. On Svelte EVERY child is a snippet whether the author wrote one or not, so arity is the only thing separating the two',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
id: 'render-prop-child',
|
|
122
|
-
what: 'children take a parameter',
|
|
123
|
-
expected: 'refuse',
|
|
124
|
-
why: 'the parameter is the press state, and tier 2 resolves that state BELOW the framework where the template cannot read it back',
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'spread-attributes',
|
|
128
|
-
what: 'the element carries a spread',
|
|
129
|
-
expected: 'refuse',
|
|
130
|
-
why: 'the attribute set cannot be enumerated, and a half-read set is a silently wrong render',
|
|
131
|
-
},
|
|
132
|
-
// THE ROW THAT PROVES A CATEGORY IS A DICTIONARY, NOT AN ENFORCEMENT POINT.
|
|
133
|
-
//
|
|
134
|
-
// `REFUSAL_CATEGORIES.bagFold` said an element carrying `role` / `aria-*` must refuse, because
|
|
135
|
-
// the fold needs the whole bag and a transform reads one attribute at a time. Measured
|
|
136
|
-
// 2026-08-31, ONE of the four transforms implemented it — Solid. Vue's two lowered such elements
|
|
137
|
-
// and Svelte's preprocessor does not contain the string `role` at all.
|
|
138
|
-
//
|
|
139
|
-
// That was not a dead refusal, it was a live defect: a lowered `aria-label` reached Fabric as a
|
|
140
|
-
// key no ViewConfig knows, so the accessibility LABEL was silently dropped on device. A category
|
|
141
|
-
// written in the shared spec binds nobody — each transform separately decides to consult it, and
|
|
142
|
-
// not consulting it breaks nothing visible. Only a ROW here makes a divergence red.
|
|
143
|
-
//
|
|
144
|
-
// The verdict is `lower` because the fold now runs in the engine (`core/engine/src/
|
|
145
|
-
// accessibility-props.ts`, called from `fabricProps` — the one point where the whole bag is known
|
|
146
|
-
// on every commit path), so the reason to refuse is gone for every adapter at once. A transform
|
|
147
|
-
// still refusing is not being safe, it is losing coverage on the props real apps write.
|
|
148
|
-
// THE TAG CHOICE ITSELF IS DELIBERATELY NOT A ROW, and the reason is this table's own admission
|
|
149
|
-
// test (`.claude/rules/adapter-parity-audit.md`). A row's verdict vocabulary is `lower` /
|
|
150
|
-
// `refuse`; it cannot say WHICH intrinsic was emitted. So a row asserting that
|
|
151
|
-
// `<TextInput multiline />` lowers would pass against a transform emitting the single-line tag —
|
|
152
|
-
// and `symbiote-text-input` is a PREFIX of `symbiote-text-input-multiline`, so even a
|
|
153
|
-
// hand-written `toContain` check reads the wrong one as right. The tag choice is pinned by each
|
|
154
|
-
// adapter's own test, where the emitted text is available; all three carry one.
|
|
155
|
-
//
|
|
156
|
-
// What the table CAN decide is the refusal, and that is what these two rows are for: a dynamic
|
|
157
|
-
// selector must refuse on every transform, or one of them commits the wrong NATIVE VIEW — an
|
|
158
|
-
// error no later prop write can correct, unlike a merely wrong prop value.
|
|
159
|
-
{
|
|
160
|
-
id: 'intrinsic-choice-dynamic',
|
|
161
|
-
what: 'the intrinsic-selecting prop is a runtime value',
|
|
162
|
-
expected: 'refuse',
|
|
163
|
-
why: 'a transform prints a static tag, so a selector it cannot resolve at compile time leaves it guessing which native view to commit',
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
id: 'intrinsic-choice-nonboolean-literal',
|
|
167
|
-
what: 'the intrinsic-selecting prop is a truthy non-boolean literal',
|
|
168
|
-
expected: 'refuse',
|
|
169
|
-
why: 'the boundary is IDENTITY, not truthiness — a type-shaped check waves `multiline={1}` through and commits the multiline view for an author who wrote a number',
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
id: 'aria-bag-fold',
|
|
173
|
-
what: 'the element carries role / aria-* attributes',
|
|
174
|
-
expected: 'lower',
|
|
175
|
-
why: 'the fold moved to the engine, so a lowered element gets it too — a transform that still refuses only costs coverage, and three of the four never refused in the first place',
|
|
176
|
-
},
|
|
177
|
-
// THE VERDICT IS RIGHT AND THE REASON THIS ROW SHIPPED WITH WAS FALSE — kept as a comment because
|
|
178
|
-
// the correction is the more useful half. It read "a lowered element has no component instance
|
|
179
|
-
// for the binding to target", which assumes the binding targeted one before. It did not: NO
|
|
180
|
-
// adapter exposes a public `ref` on `Pressable`. React's `ref: viewRef`
|
|
181
|
-
// (`components/pressable/index.ts:204`) is internal, handed to the inner View so the machine can
|
|
182
|
-
// measure its retention region; Vue and Svelte declare none, and Solid's own props type says so
|
|
183
|
-
// out loud. So `ref={handle}` on an un-lowered `<Pressable>` does nothing at all.
|
|
184
|
-
//
|
|
185
|
-
// What lowering does is therefore not to BREAK the binding but to ADD one — the intrinsic hands
|
|
186
|
-
// back a live engine node. That is the actual hazard, and it is worse than the stated one: the
|
|
187
|
-
// capability would exist only when the transform happened to lower, so an unrelated attribute
|
|
188
|
-
// elsewhere on the tag would decide whether an app's `ref` works. A surface that flickers with a
|
|
189
|
-
// compiler's verdict is harder to reason about than one that is absent everywhere.
|
|
190
|
-
//
|
|
191
|
-
// Hence the rule this row now stands on, which generalises past `ref`: A LOWERING TRANSFORM IS AN
|
|
192
|
-
// OPTIMISATION, AND AN OPTIMISATION THAT CHANGES THE OBSERVABLE SURFACE — IN EITHER DIRECTION — IS
|
|
193
|
-
// A BUG. Refusing keeps lowered and un-lowered call sites indistinguishable to an app. If
|
|
194
|
-
// `Pressable` is later given a public ref, it is given one on all five adapters by design
|
|
195
|
-
// (`<adapters_reach_full_feature_parity>`), and only then can this row be revisited.
|
|
196
|
-
//
|
|
197
|
-
// Found by the Solid session 2026-08-30, when Solid's newly-added runner answered `lower` here
|
|
198
|
-
// and the investigation went looking for the instance the row assumed.
|
|
199
|
-
{
|
|
200
|
-
id: 'instance-bound-directive',
|
|
201
|
-
what: 'the element carries a directive binding the component instance',
|
|
202
|
-
expected: 'refuse',
|
|
203
|
-
why: 'lowering must not change the observable surface: no adapter exposes a public ref on Pressable, so lowering would ADD one that appears only when the transform happens to lower',
|
|
204
|
-
},
|
|
205
|
-
// The first FOLD-ONLY primitive, and the row exists because a transform genuinely decides it: an
|
|
206
|
-
// entry can be present in the spec and still be dropped between the file and the transform's own
|
|
207
|
-
// projection (`spec-projection-covers-fields.test.ts` exists because `intrinsicWhen` was), in
|
|
208
|
-
// which case the tag is simply never recognised and the element stays a component. Nothing else
|
|
209
|
-
// in this table would catch that.
|
|
210
|
-
//
|
|
211
|
-
// No attribute of its own on purpose: what is under test is that the NAME is recognised, not any
|
|
212
|
-
// rule about a prop. A row that needed an attribute would be testing two things at once.
|
|
213
|
-
{
|
|
214
|
-
id: 'image-fold-only',
|
|
215
|
-
what: 'a fold-only primitive with no attribute worth refusing',
|
|
216
|
-
expected: 'lower',
|
|
217
|
-
why: 'the spec carries the entry and the behavior carries the fold, so every transform should recognise the tag; a refusal here means the entry never reached this transform projection',
|
|
218
|
-
},
|
|
219
|
-
// A SECOND fold-only name, and it is not a duplicate of the row above. What that one proves is
|
|
220
|
-
// that a transform's spec projection works at all; what this proves is that it is driven by the
|
|
221
|
-
// spec rather than by a hardcoded list of names — the shape `adapters/angular` shipped for months
|
|
222
|
-
// (`LOWERABLE_NAMES = ['View', 'Text']`). A transform can pass `image-fold-only` and fail here.
|
|
223
|
-
{
|
|
224
|
-
id: 'input-accessory-view-fold-only',
|
|
225
|
-
what: 'a second fold-only primitive, proving the name list is read rather than written',
|
|
226
|
-
expected: 'lower',
|
|
227
|
-
why: 'nothing about this primitive is refusable — no state, no intrinsic choice, no aliasing — so a refusal can only mean the transform never saw the entry',
|
|
228
|
-
},
|
|
229
|
-
// A THIRD name, and it is not fold-only the way the two above are — it carries a real ENGINE
|
|
230
|
-
// machine (mirrors the last value native reported, sends a platform snap-back command on
|
|
231
|
-
// disagreement). What decides a TRANSFORM's verdict is `observesState`/`intrinsicWhen`, neither
|
|
232
|
-
// of which this entry sets (its public surface has no function-valued style and no dynamic
|
|
233
|
-
// intrinsic choice), so from a transform's perspective it is answered exactly like a fold-only
|
|
234
|
-
// primitive — the machine is an engine-side fact, invisible to this row. `thumbColor` is a
|
|
235
|
-
// CONSUMED alias (folds to `thumbTintColor`), same rationale as Image's `alt`.
|
|
236
|
-
{
|
|
237
|
-
id: 'switch-fold-only',
|
|
238
|
-
what: 'a third name with an engine machine but no compile-time refusal — the verdict does not depend on it',
|
|
239
|
-
expected: 'lower',
|
|
240
|
-
why: 'no observesState, no intrinsicWhen — a refusal here can only mean the transform never saw the entry, same as the two fold-only rows above',
|
|
241
|
-
},
|
|
242
|
-
];
|
|
243
|
-
|
|
244
|
-
// NO `safe-area-view` ROW, deliberately, and the reason belongs here or the next reader files it as
|
|
245
|
-
// a coverage gap. SafeAreaView is a third fold-only primitive, and the two rows above already spend
|
|
246
|
-
// that question: one proves a transform's spec projection works at all, the other proves the name
|
|
247
|
-
// list is READ rather than written. A third attribute-free name is answered identically by every
|
|
248
|
-
// implementation, including a broken one — the admission test in
|
|
249
|
-
// `.claude/rules/adapter-parity-audit.md` ("could a transform get this wrong?") says that is a row
|
|
250
|
-
// which proves nothing.
|
|
251
|
-
//
|
|
252
|
-
// It does carry one thing genuinely new — it is the first entry declaring `aliases: {}`, so a
|
|
253
|
-
// transform that folded `id -> nativeID` unconditionally would be wrong on it. That is NOT
|
|
254
|
-
// expressible here: this table's verdict is lower/refuse, and both the right and the wrong transform
|
|
255
|
-
// LOWER. It needs a payload oracle, which is per-adapter by construction; Svelte's is the pair in
|
|
256
|
-
// `preprocessor/lower-host-primitives.test.ts` ("folds no alias on a primitive whose spec declares
|
|
257
|
-
// none", with the control beside it).
|
|
258
|
-
|
|
259
|
-
module.exports = { LOWERING_CASES };
|
package/lowering-fixtures.d.cts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Hand-written types for the loose `.cjs` beside this file — Babel/Metro transforms cannot import
|
|
2
|
-
// `.ts`, so the table stays CommonJS and its shape is declared here.
|
|
3
|
-
|
|
4
|
-
export type IVerdict = 'lower' | 'refuse';
|
|
5
|
-
|
|
6
|
-
export interface ILoweringCase {
|
|
7
|
-
/** Stable key each adapter's test maps to its own snippet. */
|
|
8
|
-
id: string;
|
|
9
|
-
/** One line naming the construct, for a failure message that reads without the table open. */
|
|
10
|
-
what: string;
|
|
11
|
-
/** The answer every transform must give. */
|
|
12
|
-
expected: IVerdict;
|
|
13
|
-
/** Why that is the answer — the reasoning a transform author needs, not a restatement. */
|
|
14
|
-
why: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export declare const LOWERING_CASES: ReadonlyArray<ILoweringCase>;
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// Turn `style={({ pressed }) => …}` into two plain style objects, so a Pressable that reads its own
|
|
2
|
-
// press state in the template can still be lowered to an intrinsic tag.
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS EXISTS AT ALL. The alternative we shipped first is a `:active` CSS rule, and it works —
|
|
5
|
-
// but it asks a developer to abandon a genuine best-practice. A ternary in a style callback is how
|
|
6
|
-
// dynamic styling is written across this ecosystem, in this project's own examples and at large
|
|
7
|
-
// engineering organisations. A tool that demands a rewrite to be fast is a tool with a defect, so
|
|
8
|
-
// the compiler does the conversion instead of the human.
|
|
9
|
-
//
|
|
10
|
-
// IT IS A SUBSTITUTION, NOT AN EVALUATION, and that distinction is the whole safety argument. No
|
|
11
|
-
// user code runs at build time. The arrow's body is cloned twice, the state identifier is replaced
|
|
12
|
-
// by `true` in one copy and `false` in the other, and conditionals whose test has become a literal
|
|
13
|
-
// are folded. Anything else in the body — `color`, `props.x`, a call — is carried through as an
|
|
14
|
-
// expression in both copies, untouched. That is why a runtime prop survives the transform.
|
|
15
|
-
//
|
|
16
|
-
// WHY IT IS SAFE TO BE EXHAUSTIVE: the callback's argument is OURS. We declare `pressed`, so we
|
|
17
|
-
// know its domain is exactly {true, false} and two specialisations cover it completely. A callback
|
|
18
|
-
// whose argument domain we did not own could not be handled this way at all.
|
|
19
|
-
|
|
20
|
-
// The state keys a primitive exposes to a style callback. One today; the mechanism generalises to
|
|
21
|
-
// any FINITE domain we declare, which is the property that makes exhaustive specialisation legal.
|
|
22
|
-
const STATE_KEYS = new Set(['pressed']);
|
|
23
|
-
|
|
24
|
-
function singleReturnedObject(body, types) {
|
|
25
|
-
if (types.isObjectExpression(body)) return body;
|
|
26
|
-
if (!types.isBlockStatement(body) || body.body.length !== 1) return null;
|
|
27
|
-
const [statement] = body.body;
|
|
28
|
-
return types.isReturnStatement(statement) &&
|
|
29
|
-
types.isObjectExpression(statement.argument)
|
|
30
|
-
? statement.argument
|
|
31
|
-
: null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// `({ pressed }) => …` only. An Identifier param (`state => state.pressed`) is a legal shape we
|
|
35
|
-
// deliberately do not accept yet: it needs member-expression substitution, which is a second
|
|
36
|
-
// mechanism, and refusing costs a call site rather than correctness.
|
|
37
|
-
function destructuredStateKeys(param, types) {
|
|
38
|
-
if (!types.isObjectPattern(param)) return null;
|
|
39
|
-
const keys = [];
|
|
40
|
-
for (const property of param.properties) {
|
|
41
|
-
if (
|
|
42
|
-
!types.isObjectProperty(property) ||
|
|
43
|
-
property.computed ||
|
|
44
|
-
!types.isIdentifier(property.key) ||
|
|
45
|
-
!types.isIdentifier(property.value) ||
|
|
46
|
-
property.key.name !== property.value.name ||
|
|
47
|
-
!STATE_KEYS.has(property.key.name)
|
|
48
|
-
) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
keys.push(property.key.name);
|
|
52
|
-
}
|
|
53
|
-
return keys.length > 0 ? keys : null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// A nested function could REDECLARE the state name, and then substituting it would rewrite a
|
|
57
|
-
// binding that has nothing to do with the press state. Cheaper to refuse than to scope-track.
|
|
58
|
-
function containsFunction(node, types) {
|
|
59
|
-
let found = false;
|
|
60
|
-
walk(node, child => {
|
|
61
|
-
if (
|
|
62
|
-
types.isFunctionExpression(child) ||
|
|
63
|
-
types.isArrowFunctionExpression(child) ||
|
|
64
|
-
types.isFunctionDeclaration(child)
|
|
65
|
-
) {
|
|
66
|
-
found = true;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
return found;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function walk(node, visit) {
|
|
73
|
-
if (node === null || typeof node !== 'object') return;
|
|
74
|
-
if (Array.isArray(node)) {
|
|
75
|
-
for (const item of node) walk(item, visit);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (typeof node.type !== 'string') return;
|
|
79
|
-
visit(node);
|
|
80
|
-
for (const key of Object.keys(node)) {
|
|
81
|
-
if (key === 'loc' || key === 'start' || key === 'end') continue;
|
|
82
|
-
walk(node[key], visit);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Rewrites in place on an already-cloned tree. A property KEY named `pressed` is not a reference to
|
|
87
|
-
// the parameter, and neither is `x.pressed` — both are skipped, which is why this walks parents
|
|
88
|
-
// rather than blindly matching identifiers.
|
|
89
|
-
function substitute(node, name, literal, types) {
|
|
90
|
-
walk(node, parent => {
|
|
91
|
-
for (const key of Object.keys(parent)) {
|
|
92
|
-
const value = parent[key];
|
|
93
|
-
if (Array.isArray(value)) {
|
|
94
|
-
value.forEach((item, index) => {
|
|
95
|
-
if (isStateRef(parent, key, item, name, types))
|
|
96
|
-
value[index] = literal();
|
|
97
|
-
});
|
|
98
|
-
} else if (isStateRef(parent, key, value, name, types)) {
|
|
99
|
-
parent[key] = literal();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function isStateRef(parent, key, node, name, types) {
|
|
106
|
-
if (!types.isIdentifier(node) || node.name !== name) return false;
|
|
107
|
-
if (types.isObjectProperty(parent) && key === 'key' && !parent.computed)
|
|
108
|
-
return false;
|
|
109
|
-
if (
|
|
110
|
-
types.isMemberExpression(parent) &&
|
|
111
|
-
key === 'property' &&
|
|
112
|
-
!parent.computed
|
|
113
|
-
) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Bottom-up, because folding a branch can turn its parent's test into a literal too.
|
|
120
|
-
function fold(node, types) {
|
|
121
|
-
walk(node, parent => {
|
|
122
|
-
for (const key of Object.keys(parent)) {
|
|
123
|
-
const value = parent[key];
|
|
124
|
-
if (Array.isArray(value)) {
|
|
125
|
-
value.forEach((item, index) => {
|
|
126
|
-
const folded = foldOne(item, types);
|
|
127
|
-
if (folded !== null) value[index] = folded;
|
|
128
|
-
});
|
|
129
|
-
} else {
|
|
130
|
-
const folded = foldOne(value, types);
|
|
131
|
-
if (folded !== null) parent[key] = folded;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function foldOne(node, types) {
|
|
138
|
-
if (
|
|
139
|
-
node === null ||
|
|
140
|
-
typeof node !== 'object' ||
|
|
141
|
-
typeof node.type !== 'string'
|
|
142
|
-
) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
fold(node, types);
|
|
146
|
-
if (
|
|
147
|
-
types.isConditionalExpression(node) &&
|
|
148
|
-
types.isBooleanLiteral(node.test)
|
|
149
|
-
) {
|
|
150
|
-
return node.test.value ? node.consequent : node.alternate;
|
|
151
|
-
}
|
|
152
|
-
if (
|
|
153
|
-
types.isUnaryExpression(node, { operator: '!' }) &&
|
|
154
|
-
types.isBooleanLiteral(node.argument)
|
|
155
|
-
) {
|
|
156
|
-
return types.booleanLiteral(!node.argument.value);
|
|
157
|
-
}
|
|
158
|
-
if (types.isLogicalExpression(node) && types.isBooleanLiteral(node.left)) {
|
|
159
|
-
if (node.operator === '&&') return node.left.value ? node.right : node.left;
|
|
160
|
-
if (node.operator === '||') return node.left.value ? node.left : node.right;
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function specializeAt(object, keys, value, types) {
|
|
166
|
-
const clone = types.cloneNode(object, true);
|
|
167
|
-
for (const key of keys) {
|
|
168
|
-
substitute(clone, key, () => types.booleanLiteral(value), types);
|
|
169
|
-
}
|
|
170
|
-
fold(clone, types);
|
|
171
|
-
return hasStateRef(clone, keys, types) ? null : clone;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Returns the two specialisations, or null to refuse — the caller then keeps the component, which
|
|
175
|
-
// is exactly today's behaviour.
|
|
176
|
-
// A surviving REFERENCE means the body used the state somewhere the substitution could not reach,
|
|
177
|
-
// and emitting a half-specialised object would be worse than refusing. It must use the same
|
|
178
|
-
// parent-aware predicate as `substitute`, or a property named `pressed` and a `state.pressed` read
|
|
179
|
-
// both count as survivors and every such call site refuses for no reason — which is exactly what a
|
|
180
|
-
// naive identifier scan did here first.
|
|
181
|
-
function hasStateRef(node, keys, types) {
|
|
182
|
-
let found = false;
|
|
183
|
-
walk(node, parent => {
|
|
184
|
-
for (const key of Object.keys(parent)) {
|
|
185
|
-
const value = parent[key];
|
|
186
|
-
const items = Array.isArray(value) ? value : [value];
|
|
187
|
-
for (const item of items) {
|
|
188
|
-
if (
|
|
189
|
-
types.isIdentifier(item) &&
|
|
190
|
-
keys.includes(item.name) &&
|
|
191
|
-
isStateRef(parent, key, item, item.name, types)
|
|
192
|
-
) {
|
|
193
|
-
found = true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
return found;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function specializeStateStyle(expression, types) {
|
|
202
|
-
if (
|
|
203
|
-
!types.isArrowFunctionExpression(expression) &&
|
|
204
|
-
!types.isFunctionExpression(expression)
|
|
205
|
-
) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
if (expression.params.length !== 1) return null;
|
|
209
|
-
const keys = destructuredStateKeys(expression.params[0], types);
|
|
210
|
-
if (keys === null) return null;
|
|
211
|
-
const object = singleReturnedObject(expression.body, types);
|
|
212
|
-
if (object === null) return null;
|
|
213
|
-
if (containsFunction(object, types)) return null;
|
|
214
|
-
const base = specializeAt(object, keys, false, types);
|
|
215
|
-
const active = specializeAt(object, keys, true, types);
|
|
216
|
-
return base === null || active === null ? null : { base, active };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
module.exports = { specializeStateStyle, STATE_KEYS };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Hand-written types for the loose `.cjs` above — Babel/Metro transforms cannot import `.ts`, so
|
|
2
|
-
// the implementation stays CommonJS and its shape is declared here.
|
|
3
|
-
import type * as babelTypes from '@babel/types';
|
|
4
|
-
|
|
5
|
-
export interface ISpecializedStateStyle {
|
|
6
|
-
base: babelTypes.ObjectExpression;
|
|
7
|
-
active: babelTypes.ObjectExpression;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export declare function specializeStateStyle(
|
|
11
|
-
expression: babelTypes.Node,
|
|
12
|
-
types: typeof babelTypes,
|
|
13
|
-
): ISpecializedStateStyle | null;
|
|
14
|
-
|
|
15
|
-
export declare const STATE_KEYS: ReadonlySet<string>;
|