@symbiote-native/engine 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -7
- package/build/accessibility-props.js +22 -21
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/graph.d.ts +2 -0
- package/build/animated/graph.js +14 -0
- package/build/animated/host-binding.d.ts +39 -0
- package/build/animated/host-binding.js +278 -0
- package/build/animated/index.d.ts +1 -1
- package/build/animated/leaf-lifecycle.js +10 -22
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +123 -33
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +129 -182
- package/build/fabric.d.ts +10 -0
- package/build/fabric.js +40 -0
- package/build/host-access.d.ts +125 -0
- package/build/host-access.js +280 -0
- package/build/host-behavior.d.ts +107 -7
- package/build/host-behavior.js +309 -31
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +18 -10
- package/build/index.js +65 -10
- package/build/mutation-buffer.d.ts +222 -0
- package/build/mutation-buffer.js +491 -0
- package/build/native-engine.d.ts +182 -0
- package/build/native-engine.js +178 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +66 -0
- package/build/node.d.ts +191 -60
- package/build/node.js +1034 -327
- package/build/pan-responder/index.d.ts +2 -2
- package/build/pan-responder/index.js +37 -56
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/styles.d.ts +5 -1
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -42
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +307 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +232 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2478 -0
- package/cpp/SymbioteTree.h +257 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1030
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
|
@@ -1,114 +1,32 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Same root cause as transformOrigin/boxShadow: RN only parses `transform` in JS when
|
|
3
|
-
// nativeCSSParsing is off (the default), and only for a STRING input - an ARRAY is
|
|
4
|
-
// returned unchanged. symbiote forwarded a raw string once, which Android native cast
|
|
5
|
-
// to ReadableArray and crashed (`String cannot be cast to ReadableArray`). This
|
|
6
|
-
// restores the missing JS parse.
|
|
1
|
+
// RN's own processTransform, imported rather than ported.
|
|
7
2
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// Why it is needed at all: RN parses `transform` in JS only when nativeCSSParsing is off (the
|
|
4
|
+
// default), and only for a STRING - an ARRAY comes back unchanged. Symbiote forwarded a raw
|
|
5
|
+
// string once with no JS parse, and Android cast it to a ReadableArray and crashed
|
|
6
|
+
// (`String cannot be cast to ReadableArray`). This restores the missing parse.
|
|
7
|
+
//
|
|
8
|
+
// Why the try/catch: upstream validates through `invariant`, i.e. it THROWS, and a commit path
|
|
9
|
+
// must never throw - a malformed transform is the app's bug, not a frame to abort. Catching keeps
|
|
10
|
+
// that guarantee while inheriting every one of upstream's ~12 checks. The hand-written port this
|
|
11
|
+
// replaces implemented two of them, so an Animated.Value handed to a non-animated component
|
|
12
|
+
// reached Fabric as an opaque object where RN redboxes, and `{perspective: 0}`, a matrix of the
|
|
13
|
+
// wrong length and an unknown key all forwarded silently.
|
|
14
|
+
//
|
|
15
|
+
// The dev/release split is upstream's, not ours: `_validateTransforms` runs under `__DEV__`, so a
|
|
16
|
+
// Release bundle skips it exactly as RN's does, and the array comes back BY IDENTITY - which the
|
|
17
|
+
// animated / sticky-header hot path depends on, since it produces a transform array every commit.
|
|
13
18
|
import { dlog } from '../debug.js';
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// RN processTransform.js:59: splits one arg into [, number, , unit?].
|
|
17
|
-
const ARG_WITH_UNITS_REGEX = /([+-]?\d+(\.\d+)?)([a-zA-Z]+|%)?/g;
|
|
18
|
-
// RN processTransform.js:63: pulls every signed/decimal number out of a matrix arg list.
|
|
19
|
-
const MATRIX_NUMBER_REGEX = /[+-]?\d+(\.\d+)?/g;
|
|
20
|
-
// RN processTransform.js:52-139. Mirrors _getKeyAndValueFromCSSTransform: turns one
|
|
21
|
-
// `key(args)` pair into the entry value native expects.
|
|
22
|
-
function getKeyAndValueFromCSSTransform(key, args) {
|
|
23
|
-
switch (key) {
|
|
24
|
-
case 'matrix': {
|
|
25
|
-
// RN processTransform.js:62-63.
|
|
26
|
-
const numbers = args.match(MATRIX_NUMBER_REGEX);
|
|
27
|
-
return { key, value: numbers == null ? undefined : numbers.map(Number) };
|
|
28
|
-
}
|
|
29
|
-
case 'translate':
|
|
30
|
-
case 'translate3d': {
|
|
31
|
-
// RN processTransform.js:64-113.
|
|
32
|
-
const parsedArgs = [];
|
|
33
|
-
ARG_WITH_UNITS_REGEX.lastIndex = 0;
|
|
34
|
-
let matches;
|
|
35
|
-
while ((matches = ARG_WITH_UNITS_REGEX.exec(args))) {
|
|
36
|
-
const value = Number(matches[1]);
|
|
37
|
-
const unitOfMeasurement = matches[3];
|
|
38
|
-
if (value !== 0 && !unitOfMeasurement) {
|
|
39
|
-
dlog(`processTransform: ${key}(${args}) length must have a unit unless 0`);
|
|
40
|
-
}
|
|
41
|
-
if (unitOfMeasurement === '%') {
|
|
42
|
-
parsedArgs.push(`${value}%`);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
parsedArgs.push(value);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// RN processTransform.js:109-111: a single-axis translate gets an implicit y of 0.
|
|
49
|
-
if (parsedArgs.length === 1) {
|
|
50
|
-
parsedArgs.push(0);
|
|
51
|
-
}
|
|
52
|
-
// RN normalizes translate3d down to the `translate` key.
|
|
53
|
-
return { key: 'translate', value: parsedArgs };
|
|
54
|
-
}
|
|
55
|
-
case 'translateX':
|
|
56
|
-
case 'translateY':
|
|
57
|
-
case 'perspective': {
|
|
58
|
-
// RN processTransform.js:114-134.
|
|
59
|
-
ARG_WITH_UNITS_REGEX.lastIndex = 0;
|
|
60
|
-
const argMatches = ARG_WITH_UNITS_REGEX.exec(args);
|
|
61
|
-
if (argMatches == null || argMatches.length === 0) {
|
|
62
|
-
return { key, value: undefined };
|
|
63
|
-
}
|
|
64
|
-
const value = Number(argMatches[1]);
|
|
65
|
-
const unitOfMeasurement = argMatches[3];
|
|
66
|
-
if (value !== 0 && !unitOfMeasurement) {
|
|
67
|
-
dlog(`processTransform: ${key}(${args}) must have a unit unless 0`);
|
|
68
|
-
}
|
|
69
|
-
return { key, value };
|
|
70
|
-
}
|
|
71
|
-
default:
|
|
72
|
-
// RN processTransform.js:136-137: a numeric arg (scale, rotate '10' would be NaN)
|
|
73
|
-
// becomes a number; an angle string ('6deg', '1.16rad') stays a string.
|
|
74
|
-
return { key, value: isNaN(Number(args)) ? args : Number(args) };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// RN processTransform.js:24-50. String input is parsed into the entry array; array
|
|
78
|
-
// input is returned unchanged (see file header for why).
|
|
19
|
+
// @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
|
|
20
|
+
import processTransformUpstream from 'react-native/Libraries/StyleSheet/processTransform';
|
|
79
21
|
export function processTransform(transform) {
|
|
80
|
-
if (transform == null)
|
|
22
|
+
if (transform == null)
|
|
81
23
|
return [];
|
|
24
|
+
try {
|
|
25
|
+
return processTransformUpstream(transform);
|
|
82
26
|
}
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
TRANSFORM_REGEX.lastIndex = 0;
|
|
89
|
-
const transformArray = [];
|
|
90
|
-
let matches;
|
|
91
|
-
while ((matches = TRANSFORM_REGEX.exec(transform))) {
|
|
92
|
-
const { key, value } = getKeyAndValueFromCSSTransform(matches[1], matches[2]);
|
|
93
|
-
if (value !== undefined) {
|
|
94
|
-
transformArray.push({ [key]: value });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return transformArray;
|
|
98
|
-
}
|
|
99
|
-
// RN processTransform.js:141-159 (_validateTransforms) rewritten to NEVER throw: it only
|
|
100
|
-
// dlogs the same conditions RN's invariant would have flagged, then returns. The commit
|
|
101
|
-
// path keeps the array regardless: an invalid transform is the caller's bug, not ours to
|
|
102
|
-
// abort a frame over.
|
|
103
|
-
function warnInvalidTransforms(transform) {
|
|
104
|
-
for (const transformation of transform) {
|
|
105
|
-
const keys = Object.keys(transformation);
|
|
106
|
-
if (keys.length !== 1) {
|
|
107
|
-
dlog(`processTransform: each transform object must have exactly one key, got ${keys.length}`);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (keys[0] === 'matrix' && transform.length > 1) {
|
|
111
|
-
dlog('processTransform: a matrix transform must be the only transform in the list');
|
|
112
|
-
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// Upstream's message names the offending key and value, so it is worth more than ours was.
|
|
29
|
+
dlog(`processTransform: dropped an invalid transform - ${error instanceof Error ? error.message : String(error)}`);
|
|
30
|
+
return [];
|
|
113
31
|
}
|
|
114
32
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
type ITransformOriginValue = string | number;
|
|
2
|
-
export declare function processTransformOrigin(transformOrigin: Array<ITransformOriginValue> | string | undefined): Array<ITransformOriginValue
|
|
2
|
+
export declare function processTransformOrigin(transformOrigin: Array<ITransformOriginValue> | string | undefined): Array<ITransformOriginValue> | undefined;
|
|
3
3
|
export {};
|
|
@@ -1,108 +1,35 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Same root cause as boxShadow/filter: ReactNativeStyleAttributes registers `transformOrigin`
|
|
3
|
-
// with `nativeCSSParsing ? true : {process: processTransformOrigin}`, and enableNativeCSSParsing()
|
|
4
|
-
// DEFAULTS TO FALSE, so RN's stock path runs processTransformOrigin IN JS, turning the CSS
|
|
5
|
-
// string ('top left') into the [x, y, z] array native expects. symbiote forwarded the raw
|
|
6
|
-
// string: iOS native tolerated it, Android native casts it to ReadableArray and crashes with
|
|
7
|
-
// `java.lang.String cannot be cast to com.facebook.react.bridge.ReadableArray`. This restores
|
|
8
|
-
// the missing JS parse so native always receives the [x, y, z] array.
|
|
1
|
+
// RN's own processTransformOrigin, imported rather than ported.
|
|
9
2
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
3
|
+
// Why it is needed at all: ReactNativeStyleAttributes registers `transformOrigin` with
|
|
4
|
+
// `nativeCSSParsing ? true : {process: processTransformOrigin}`, and enableNativeCSSParsing()
|
|
5
|
+
// DEFAULTS TO FALSE - so RN's stock path parses the CSS string ('top left') into the [x, y, z]
|
|
6
|
+
// array native expects. Symbiote forwarded the raw string: iOS tolerated it, Android cast it to a
|
|
7
|
+
// ReadableArray and crashed. This restores the missing parse.
|
|
8
|
+
//
|
|
9
|
+
// Why the try/catch: upstream rejects a malformed origin through `invariant`, i.e. it THROWS, and
|
|
10
|
+
// a commit path must never throw. Which of its invariants survive a Release build is worth being
|
|
11
|
+
// precise about, because this comment said "these invariants are NOT __DEV__-gated" and that is
|
|
12
|
+
// only half true: `_validateTransformOrigin` IS gated (processTransformOrigin.js:115), while the
|
|
13
|
+
// four invariants inside the parse loop (:42, :52, :79, :91) are not. '50% left' hits an ungated
|
|
14
|
+
// one - a horizontal keyword in the y slot - so upstream does refuse it in Release, and the
|
|
15
|
+
// try/catch is load-bearing there. A malformed ARRAY, by contrast, is only caught in dev.
|
|
16
|
+
//
|
|
17
|
+
// The hand-written port this replaces kept whatever it had parsed and returned a PARTIAL origin.
|
|
18
|
+
// That is not the safer option it looks like: a partial origin is a real, wrong origin, applied
|
|
19
|
+
// silently. `undefined` is the honest answer, and it is what the payload builder drops.
|
|
12
20
|
import { dlog } from '../debug.js';
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
// RN processTransformOrigin.js:16-18.
|
|
16
|
-
const INDEX_X = 0;
|
|
17
|
-
const INDEX_Y = 1;
|
|
18
|
-
const INDEX_Z = 2;
|
|
19
|
-
// RN processTransformOrigin.js:21-120. Parses the CSS string into the [x, y, z] array,
|
|
20
|
-
// or normalizes/passes through an array input unchanged.
|
|
21
|
+
// @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
|
|
22
|
+
import processTransformOriginUpstream from 'react-native/Libraries/StyleSheet/processTransformOrigin';
|
|
21
23
|
export function processTransformOrigin(transformOrigin) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Array input passes through, like RN (which only re-validates in __DEV__).
|
|
29
|
-
return transformOrigin;
|
|
24
|
+
// Absent input means an absent prop. The port answered with the CSS default here, so
|
|
25
|
+
// `transformOrigin: null` committed a real center origin - a write RN never makes.
|
|
26
|
+
if (transformOrigin == null)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return processTransformOriginUpstream(transformOrigin);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let index = INDEX_X;
|
|
35
|
-
let matches;
|
|
36
|
-
outer: while ((matches = TRANSFORM_ORIGIN_REGEX.exec(transformOriginString))) {
|
|
37
|
-
let nextIndex = index + 1;
|
|
38
|
-
const value = matches[0];
|
|
39
|
-
const valueLower = value.toLowerCase();
|
|
40
|
-
switch (valueLower) {
|
|
41
|
-
case 'left':
|
|
42
|
-
case 'right': {
|
|
43
|
-
// RN processTransformOrigin.js:42-46, left/right are x-only.
|
|
44
|
-
if (index !== INDEX_X) {
|
|
45
|
-
dlog(`processTransformOrigin reject: "${value}" can only be used for x-position`);
|
|
46
|
-
return transformOriginArray;
|
|
47
|
-
}
|
|
48
|
-
transformOriginArray[INDEX_X] = valueLower === 'left' ? 0 : '100%';
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case 'top':
|
|
52
|
-
case 'bottom': {
|
|
53
|
-
// RN processTransformOrigin.js:52-56, top/bottom are not valid for z.
|
|
54
|
-
if (index === INDEX_Z) {
|
|
55
|
-
dlog(`processTransformOrigin reject: "${value}" can only be used for y-position`);
|
|
56
|
-
return transformOriginArray;
|
|
57
|
-
}
|
|
58
|
-
transformOriginArray[INDEX_Y] = valueLower === 'top' ? 0 : '100%';
|
|
59
|
-
// RN processTransformOrigin.js:59-86, handle [[ center | left | right ] &&
|
|
60
|
-
// [ center | top | bottom ]] <length>? When y came first, the next token is x.
|
|
61
|
-
if (index === INDEX_X) {
|
|
62
|
-
const horizontal = TRANSFORM_ORIGIN_REGEX.exec(transformOriginString);
|
|
63
|
-
if (horizontal == null) {
|
|
64
|
-
break outer;
|
|
65
|
-
}
|
|
66
|
-
switch (horizontal[0].toLowerCase()) {
|
|
67
|
-
case 'left':
|
|
68
|
-
transformOriginArray[INDEX_X] = 0;
|
|
69
|
-
break;
|
|
70
|
-
case 'right':
|
|
71
|
-
transformOriginArray[INDEX_X] = '100%';
|
|
72
|
-
break;
|
|
73
|
-
case 'center':
|
|
74
|
-
transformOriginArray[INDEX_X] = '50%';
|
|
75
|
-
break;
|
|
76
|
-
default:
|
|
77
|
-
dlog(`processTransformOrigin reject: could not parse "${transformOriginString}"`);
|
|
78
|
-
return transformOriginArray;
|
|
79
|
-
}
|
|
80
|
-
nextIndex = INDEX_Z;
|
|
81
|
-
}
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
case 'center': {
|
|
85
|
-
// RN processTransformOrigin.js:91-95, center is invalid for z.
|
|
86
|
-
if (index === INDEX_Z) {
|
|
87
|
-
dlog(`processTransformOrigin reject: "${value}" cannot be used for z-position`);
|
|
88
|
-
return transformOriginArray;
|
|
89
|
-
}
|
|
90
|
-
transformOriginArray[index] = '50%';
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
default: {
|
|
94
|
-
// RN processTransformOrigin.js:99-105, a percentage stays a string, a length
|
|
95
|
-
// drops its `px` and becomes a number.
|
|
96
|
-
if (value.endsWith('%')) {
|
|
97
|
-
transformOriginArray[index] = value;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
transformOriginArray[index] = parseFloat(value);
|
|
101
|
-
}
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
index = nextIndex;
|
|
31
|
+
catch (error) {
|
|
32
|
+
dlog(`processTransformOrigin: dropped an invalid origin - ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
return undefined;
|
|
106
34
|
}
|
|
107
|
-
return transformOriginArray;
|
|
108
35
|
}
|
package/build/registry.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IPayloadFold } from './host-behavior';
|
|
1
2
|
export type IPropProcessor = (value: unknown) => unknown;
|
|
2
3
|
export interface INativeEventBinding {
|
|
3
4
|
raw: string;
|
|
@@ -28,4 +29,39 @@ export declare function registerComponent(name: string, registration?: IComponen
|
|
|
28
29
|
export declare function isRegisteredEvent(component: string, listener: string): boolean;
|
|
29
30
|
export declare function registeredNativeEvent(component: string, raw: string): INativeEventBinding | undefined;
|
|
30
31
|
export declare function registeredProcessor(component: string, key: string): IPropProcessor | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* A component's ViewConfig processors, as a payload fold — or undefined when it has none.
|
|
34
|
+
*
|
|
35
|
+
* WHY A FOLD AND NOT A LOOKUP AT PAYLOAD-BUILD TIME. The payload is built in C++ on a device
|
|
36
|
+
* (`core/engine/cpp/SymbioteFabricProps.cpp`) and only in JS headless, and this registry cannot
|
|
37
|
+
* cross that boundary: it is populated lazily from an INJECTED `ReactNativeViewConfigRegistry`
|
|
38
|
+
* lookup, holding JS closures. `payloadFold` is the one seam that already runs in JS on both paths
|
|
39
|
+
* — the C++ probes it once per node and calls back — so putting the processors there is what makes
|
|
40
|
+
* a third-party view behave the same on a device as it does in a test.
|
|
41
|
+
*
|
|
42
|
+
* The alternative was a list of prop NAMES in C++, and that is what this replaces. It cost a day:
|
|
43
|
+
* `@symbiote-native/slider` declares `minimumTrackTintColor` / `maximumTrackTintColor` in its own
|
|
44
|
+
* ViewConfig, neither name was in the C++ list, both reached Fabric as CSS strings, and iOS answers
|
|
45
|
+
* a string colour with `clearColor()`. The slider dragged and reported values correctly with no
|
|
46
|
+
* track drawn at all. Any list of names is a list somebody has to extend for a component we have
|
|
47
|
+
* never seen — which would have meant editing C++ to add a native view, and that is exactly the
|
|
48
|
+
* coupling `<native_core_is_untouched>` exists to prevent.
|
|
49
|
+
*
|
|
50
|
+
* Resolved ONCE per component and cached, because `createElement` asks per node. A built-in
|
|
51
|
+
* short-circuits inside `resolve` before any of this.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The prop names a component's OWN ViewConfig claims a processor for — i.e. exactly the keys
|
|
55
|
+
* `configPayloadFold` has already converted by the time the payload builder's own passes run.
|
|
56
|
+
*
|
|
57
|
+
* It exists so "a colour is converted exactly once" can be a stated rule rather than a lucky one.
|
|
58
|
+
* The overlap is real: `thumbTintColor` is claimed by @symbiote-native/slider's config AND by the
|
|
59
|
+
* engine's built-in COLOR_PROPS, and for a while nothing broke only because a processed colour came
|
|
60
|
+
* back as a NUMBER and the engine skipped numbers. That guard died when a numeric colour became
|
|
61
|
+
* processable in its own right — an author writing `color: 0xff0000ff` means rrggbbaa and owes the
|
|
62
|
+
* same rotation a string owes — and the second conversion then turned an already-correct int into
|
|
63
|
+
* a different colour.
|
|
64
|
+
*/
|
|
65
|
+
export declare function configProcessedKeys(component: string): ReadonlySet<string> | undefined;
|
|
66
|
+
export declare function configPayloadFold(component: string): IPayloadFold | undefined;
|
|
31
67
|
export {};
|
package/build/registry.js
CHANGED
|
@@ -48,11 +48,17 @@ const BUILTIN_COMPONENTS = new Set([
|
|
|
48
48
|
// Manual overrides per component (usually none): the escape hatch.
|
|
49
49
|
const overrides = new Map();
|
|
50
50
|
const resolvedCache = new Map();
|
|
51
|
+
// `configPayloadFold`'s answer, boxed so a component with no processors caches its `undefined`
|
|
52
|
+
// instead of re-resolving on every `createElement`.
|
|
53
|
+
const foldCache = new Map();
|
|
54
|
+
// Same boxing, for the key set `configProcessedKeys` answers.
|
|
55
|
+
const keysCache = new Map();
|
|
51
56
|
let viewConfigSource;
|
|
52
57
|
// Wired once by the adapter on a real host: `name => ReactNativeViewConfigRegistry.get(name)`.
|
|
53
58
|
export function setNativeViewConfigSource(source) {
|
|
54
59
|
viewConfigSource = source;
|
|
55
60
|
resolvedCache.clear();
|
|
61
|
+
foldCache.clear();
|
|
56
62
|
}
|
|
57
63
|
// Escape hatch: override a derived config, or supply one for a view with no codegen
|
|
58
64
|
// ViewConfig. NOT needed on the common path; views derive from the source.
|
|
@@ -63,6 +69,7 @@ export function registerComponent(name, registration = {}) {
|
|
|
63
69
|
else
|
|
64
70
|
list.push(registration);
|
|
65
71
|
resolvedCache.delete(name);
|
|
72
|
+
foldCache.delete(name);
|
|
66
73
|
}
|
|
67
74
|
// onChange -> change (mirrors node.ts listenerName; the split of the handler prop).
|
|
68
75
|
function splitListener(handlerProp) {
|
|
@@ -149,3 +156,69 @@ export function registeredNativeEvent(component, raw) {
|
|
|
149
156
|
export function registeredProcessor(component, key) {
|
|
150
157
|
return resolve(component).processors.get(key);
|
|
151
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* A component's ViewConfig processors, as a payload fold — or undefined when it has none.
|
|
161
|
+
*
|
|
162
|
+
* WHY A FOLD AND NOT A LOOKUP AT PAYLOAD-BUILD TIME. The payload is built in C++ on a device
|
|
163
|
+
* (`core/engine/cpp/SymbioteFabricProps.cpp`) and only in JS headless, and this registry cannot
|
|
164
|
+
* cross that boundary: it is populated lazily from an INJECTED `ReactNativeViewConfigRegistry`
|
|
165
|
+
* lookup, holding JS closures. `payloadFold` is the one seam that already runs in JS on both paths
|
|
166
|
+
* — the C++ probes it once per node and calls back — so putting the processors there is what makes
|
|
167
|
+
* a third-party view behave the same on a device as it does in a test.
|
|
168
|
+
*
|
|
169
|
+
* The alternative was a list of prop NAMES in C++, and that is what this replaces. It cost a day:
|
|
170
|
+
* `@symbiote-native/slider` declares `minimumTrackTintColor` / `maximumTrackTintColor` in its own
|
|
171
|
+
* ViewConfig, neither name was in the C++ list, both reached Fabric as CSS strings, and iOS answers
|
|
172
|
+
* a string colour with `clearColor()`. The slider dragged and reported values correctly with no
|
|
173
|
+
* track drawn at all. Any list of names is a list somebody has to extend for a component we have
|
|
174
|
+
* never seen — which would have meant editing C++ to add a native view, and that is exactly the
|
|
175
|
+
* coupling `<native_core_is_untouched>` exists to prevent.
|
|
176
|
+
*
|
|
177
|
+
* Resolved ONCE per component and cached, because `createElement` asks per node. A built-in
|
|
178
|
+
* short-circuits inside `resolve` before any of this.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* The prop names a component's OWN ViewConfig claims a processor for — i.e. exactly the keys
|
|
182
|
+
* `configPayloadFold` has already converted by the time the payload builder's own passes run.
|
|
183
|
+
*
|
|
184
|
+
* It exists so "a colour is converted exactly once" can be a stated rule rather than a lucky one.
|
|
185
|
+
* The overlap is real: `thumbTintColor` is claimed by @symbiote-native/slider's config AND by the
|
|
186
|
+
* engine's built-in COLOR_PROPS, and for a while nothing broke only because a processed colour came
|
|
187
|
+
* back as a NUMBER and the engine skipped numbers. That guard died when a numeric colour became
|
|
188
|
+
* processable in its own right — an author writing `color: 0xff0000ff` means rrggbbaa and owes the
|
|
189
|
+
* same rotation a string owes — and the second conversion then turned an already-correct int into
|
|
190
|
+
* a different colour.
|
|
191
|
+
*/
|
|
192
|
+
export function configProcessedKeys(component) {
|
|
193
|
+
const cached = keysCache.get(component);
|
|
194
|
+
if (cached !== undefined)
|
|
195
|
+
return cached.keys;
|
|
196
|
+
const { processors } = resolve(component);
|
|
197
|
+
const keys = processors.size === 0 ? undefined : new Set(processors.keys());
|
|
198
|
+
keysCache.set(component, { keys });
|
|
199
|
+
return keys;
|
|
200
|
+
}
|
|
201
|
+
export function configPayloadFold(component) {
|
|
202
|
+
const cached = foldCache.get(component);
|
|
203
|
+
if (cached !== undefined)
|
|
204
|
+
return cached.fold;
|
|
205
|
+
const { processors } = resolve(component);
|
|
206
|
+
const fold = processors.size === 0
|
|
207
|
+
? undefined
|
|
208
|
+
: props => {
|
|
209
|
+
// Copied only when a processor actually claims a key present in the bag: the fold
|
|
210
|
+
// contract forbids mutating `node.props`, and a component whose config declares
|
|
211
|
+
// processors for props this node never sets must still hand its input back by identity.
|
|
212
|
+
let out;
|
|
213
|
+
for (const [key, process] of processors) {
|
|
214
|
+
if (!(key in props))
|
|
215
|
+
continue;
|
|
216
|
+
const claimed = out ?? { ...props };
|
|
217
|
+
claimed[key] = process(props[key]);
|
|
218
|
+
out = claimed;
|
|
219
|
+
}
|
|
220
|
+
return out ?? props;
|
|
221
|
+
};
|
|
222
|
+
foldCache.set(component, { fold });
|
|
223
|
+
return fold;
|
|
224
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SoundManager: plays the Android system touch-sound feedback. Mirrors RN's
|
|
2
|
+
// Libraries/Components/Sound/SoundManager.js — a single method, `playTouchSound`, wrapping the
|
|
3
|
+
// TurboModule the same way (`NativeSoundManager` resolves via `TurboModuleRegistry.get`, not
|
|
4
|
+
// `getEnforcing`, so a missing module degrades to a no-op there too).
|
|
5
|
+
//
|
|
6
|
+
// The native contract, from `src/private/specs_DEPRECATED/modules/NativeSoundManager.js`:
|
|
7
|
+
// playTouchSound(): void
|
|
8
|
+
//
|
|
9
|
+
// Called from `core/components/src/state/pressable.ts`'s press machine, on Android, right before
|
|
10
|
+
// `onPress` fires, gated by `android_disableSound !== true` — Pressability.js:754-756. Not a Fabric
|
|
11
|
+
// prop: no ViewConfig declares `android_disableSound`, it only ever gates this JS-side call.
|
|
12
|
+
import { dlog } from '../debug.js';
|
|
13
|
+
import { getNativeModule } from '../native-modules/index.js';
|
|
14
|
+
const SOUND_MODULE = 'SoundManager';
|
|
15
|
+
// Lazily resolved so importing this module has no native side effect.
|
|
16
|
+
let soundModule;
|
|
17
|
+
function getModule() {
|
|
18
|
+
if (soundModule === undefined) {
|
|
19
|
+
soundModule = getNativeModule(SOUND_MODULE);
|
|
20
|
+
dlog(`SoundManager: SoundManager module ${soundModule ? 'resolved' : 'NOT resolved (null)'}`);
|
|
21
|
+
}
|
|
22
|
+
return soundModule;
|
|
23
|
+
}
|
|
24
|
+
export const SoundManager = {
|
|
25
|
+
// Degrades to a no-op (logged) when the module is absent — never throws — matching every other
|
|
26
|
+
// optional native module in this layer.
|
|
27
|
+
playTouchSound() {
|
|
28
|
+
const module = getModule();
|
|
29
|
+
if (module === null) {
|
|
30
|
+
dlog('SoundManager.playTouchSound -> SoundManager native module unavailable, no-op');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
dlog('SoundManager.playTouchSound');
|
|
34
|
+
module.playTouchSound();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A style value with its structured keys resolved — the same value by identity when there was
|
|
3
|
+
* nothing to resolve, which is nearly always.
|
|
4
|
+
*
|
|
5
|
+
* Accepts what a style slot can hold: one object, or a (nested) array of them. An array is not
|
|
6
|
+
* memoized — `pushClassStyle` mints a fresh one on every publish, so a cache keyed on it could
|
|
7
|
+
* never hit; its ENTRIES carry the memo instead, and the original array comes back untouched when
|
|
8
|
+
* none of them moved.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveStructuredStyle(style: unknown): unknown;
|