@symbiote-native/engine 0.5.0 → 1.1.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.
Files changed (91) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -8
  10. package/build/accessibility-props.js +13 -16
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +145 -0
  28. package/build/host-access.js +315 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +236 -51
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +238 -0
  38. package/build/mutation-buffer.js +513 -0
  39. package/build/native-engine.d.ts +185 -0
  40. package/build/native-engine.js +182 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +68 -0
  43. package/build/node.d.ts +195 -58
  44. package/build/node.js +852 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +322 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +234 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2593 -0
  80. package/cpp/SymbioteTree.h +294 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
@@ -1,114 +1,32 @@
1
- // JS-side port of RN's processTransform (Libraries/StyleSheet/processTransform.js).
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
- // CRITICAL: the animated / sticky-header hot path produces transform ARRAYS on every
9
- // commit flush, so the array branch MUST return the byte-identical reference (no
10
- // decompose, no clone). RN's only array-path check, _validateTransforms, is __DEV__-only
11
- // and throws via invariant; we never throw in commit, so we dlog instead and still
12
- // return the array unchanged.
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
- // RN processTransform.js:28: matches each `name(args)` in the CSS string.
15
- const TRANSFORM_REGEX = /(\w+)\(([^)]+)\)/g;
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
- if (typeof transform !== 'string') {
84
- // Hot path: return the same reference, no clone (see file header).
85
- warnInvalidTransforms(transform);
86
- return transform;
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
- // JS-side port of RN's processTransformOrigin (Libraries/StyleSheet/processTransformOrigin.js).
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
- // RN throws via invariant() on a malformed value; we instead dlog and keep the partial array
11
- // (matching the boxShadow/filter idiom of never throwing into the commit path).
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
- // RN processTransformOrigin.js:14, pre-compiled pattern matching each keyword / length token.
14
- const TRANSFORM_ORIGIN_REGEX = /(top|bottom|left|right|center|\d+(?:%|px)|0)/gi;
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
- if (transformOrigin == null) {
23
- // RN never receives undefined here (the registry only calls the processor for a
24
- // present value), but the commit path may; default to center/center/0.
25
- return ['50%', '50%', 0];
26
- }
27
- if (typeof transformOrigin !== 'string') {
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
- const transformOriginString = transformOrigin;
32
- TRANSFORM_ORIGIN_REGEX.lastIndex = 0;
33
- const transformOriginArray = ['50%', '50%', 0];
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
  }
@@ -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,3 @@
1
+ export declare const SoundManager: {
2
+ playTouchSound(): void;
3
+ };
@@ -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;