@symbiote-native/engine 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/android/CMakeLists.txt +31 -1
  2. package/android/build.gradle +4 -1
  3. package/build/accessibility-info/index.android.js +21 -17
  4. package/build/accessibility-info/index.ios.js +2 -0
  5. package/build/accessibility-info/shared.d.ts +1 -1
  6. package/build/accessibility-props.d.ts +0 -11
  7. package/build/accessibility-props.js +30 -68
  8. package/build/alert/shared.d.ts +1 -1
  9. package/build/alert/shared.js +3 -7
  10. package/build/animated/graph.js +1 -1
  11. package/build/animated/leaf-lifecycle.js +2 -2
  12. package/build/app-state/index.d.ts +1 -1
  13. package/build/app-state/index.js +35 -27
  14. package/build/asset-source-resolver.d.ts +2 -0
  15. package/build/asset-source-resolver.js +13 -0
  16. package/build/back-handler/index.d.ts +7 -6
  17. package/build/back-handler/index.js +19 -16
  18. package/build/debug.js +8 -22
  19. package/build/dispatch.js +3 -9
  20. package/build/events/index.js +12 -3
  21. package/build/fabric-props.js +75 -180
  22. package/build/fabric.d.ts +2 -8
  23. package/build/fabric.js +27 -33
  24. package/build/host-access.d.ts +1 -128
  25. package/build/host-access.js +96 -205
  26. package/build/host-behavior.d.ts +1 -100
  27. package/build/host-behavior.js +125 -311
  28. package/build/image-loader.d.ts +4 -2
  29. package/build/image-loader.js +24 -48
  30. package/build/image-source-resolver.js +3 -7
  31. package/build/image-source-write.d.ts +1 -12
  32. package/build/image-source-write.js +28 -36
  33. package/build/imperative.d.ts +0 -28
  34. package/build/imperative.js +42 -92
  35. package/build/index.d.ts +3 -2
  36. package/build/index.js +30 -43
  37. package/build/invariant.d.ts +1 -0
  38. package/build/invariant.js +10 -0
  39. package/build/keyboard/index.js +11 -32
  40. package/build/linking/index.android.js +5 -3
  41. package/build/linking/shared.d.ts +1 -1
  42. package/build/linking/shared.js +16 -19
  43. package/build/mutation-buffer.d.ts +0 -177
  44. package/build/mutation-buffer.js +137 -308
  45. package/build/native-engine.d.ts +0 -102
  46. package/build/native-engine.js +38 -98
  47. package/build/native-events.d.ts +5 -0
  48. package/build/native-events.js +30 -18
  49. package/build/native-tree-host.d.ts +0 -21
  50. package/build/native-tree-host.js +14 -31
  51. package/build/node.d.ts +0 -212
  52. package/build/node.js +354 -774
  53. package/build/permissions-android/index.android.d.ts +59 -0
  54. package/build/permissions-android/index.android.js +47 -0
  55. package/build/permissions-android/index.d.ts +1 -115
  56. package/build/permissions-android/index.ios.d.ts +59 -0
  57. package/build/permissions-android/index.ios.js +31 -0
  58. package/build/permissions-android/index.js +3 -184
  59. package/build/permissions-android/shared.d.ts +63 -0
  60. package/build/permissions-android/shared.js +66 -0
  61. package/build/platform/index.android.js +3 -5
  62. package/build/platform/index.ios.js +3 -3
  63. package/build/platform/shared.d.ts +4 -0
  64. package/build/platform/shared.js +9 -0
  65. package/build/platform-color/index.android.d.ts +4 -0
  66. package/build/platform-color/index.android.js +7 -0
  67. package/build/platform-color/index.d.ts +2 -20
  68. package/build/platform-color/index.js +1 -45
  69. package/build/platform-color/shared.d.ts +21 -0
  70. package/build/platform-color/shared.js +41 -0
  71. package/build/post-commit.js +3 -8
  72. package/build/process-aspect-ratio.js +3 -7
  73. package/build/process-background-longhands.js +10 -19
  74. package/build/process-filter.js +11 -19
  75. package/build/process-font-variant.js +3 -7
  76. package/build/registry.d.ts +0 -33
  77. package/build/registry.js +22 -57
  78. package/build/report-error.js +4 -18
  79. package/build/settings/index.android.d.ts +6 -0
  80. package/build/settings/index.android.js +21 -0
  81. package/build/settings/index.d.ts +1 -8
  82. package/build/settings/index.ios.d.ts +8 -0
  83. package/build/settings/index.ios.js +122 -0
  84. package/build/settings/index.js +3 -122
  85. package/build/share/index.android.js +9 -32
  86. package/build/share/index.ios.js +14 -15
  87. package/build/share/shared.d.ts +4 -2
  88. package/build/share/shared.js +7 -10
  89. package/build/status-bar/index.android.js +1 -1
  90. package/build/status-bar/index.ios.js +4 -3
  91. package/build/structured-style.d.ts +0 -9
  92. package/build/structured-style.js +16 -31
  93. package/build/styles.js +3 -6
  94. package/build/surface.d.ts +0 -26
  95. package/build/surface.js +29 -76
  96. package/build/text-input-state.js +4 -8
  97. package/build/toast-android/index.android.d.ts +10 -0
  98. package/build/toast-android/index.android.js +108 -0
  99. package/build/toast-android/index.d.ts +1 -10
  100. package/build/toast-android/index.ios.d.ts +10 -0
  101. package/build/toast-android/index.ios.js +19 -0
  102. package/build/toast-android/index.js +3 -108
  103. package/build/touch-history.js +5 -11
  104. package/build/tree-host.d.ts +0 -270
  105. package/build/tree-host.js +63 -153
  106. package/build/view-config.js +17 -37
  107. package/cpp/SymbioteFabricProps.cpp +204 -120
  108. package/cpp/SymbioteFabricProps.h +5 -0
  109. package/cpp/SymbioteTree.cpp +20 -6
  110. package/package.json +2 -2
@@ -3,7 +3,7 @@
3
3
  // from the PlatformConstants native module (RN spec NativePlatformConstantsAndroid.js,
4
4
  // a different payload from iOS). Metro picks this on an Android host. The module name is
5
5
  // the same 'PlatformConstants'; only the shape differs.
6
- import { createConstantsResolver, } from './shared.js';
6
+ import { createConstantsResolver, gatedIsTesting, } from './shared.js';
7
7
  // The filename already selected this host: 'android' is a literal, not a probe.
8
8
  const OS_ANDROID = 'android';
9
9
  // RN reads isTV off the uiMode constant.
@@ -37,14 +37,12 @@ export const Platform = {
37
37
  get isVision() {
38
38
  return false;
39
39
  },
40
- // RN gates this behind __DEV__; shared has no __DEV__ flag, so we read the native
41
- // flag directly: a release build's native module reports false anyway.
42
40
  get isTesting() {
43
- return resolveConstants()?.isTesting ?? false;
41
+ return gatedIsTesting(resolveConstants());
44
42
  },
45
43
  get isDisableAnimations() {
46
44
  const constants = resolveConstants();
47
- return constants?.isDisableAnimations ?? constants?.isTesting ?? false;
45
+ return constants?.isDisableAnimations ?? gatedIsTesting(constants);
48
46
  },
49
47
  get isMacCatalyst() {
50
48
  return false;
@@ -3,7 +3,7 @@
3
3
  // (RN spec NativePlatformConstantsIOS.js), read lazily and cached via the shared
4
4
  // resolver. Metro picks this on an iOS host; it is also the base re-export target
5
5
  // (platform.ts) so tsc / tsx / web (no Metro) land here too.
6
- import { createConstantsResolver, UNKNOWN_VERSION, } from './shared.js';
6
+ import { createConstantsResolver, gatedIsTesting, UNKNOWN_VERSION, } from './shared.js';
7
7
  // interfaceIdiom values RN compares against for the device-class getters.
8
8
  const IDIOM_PAD = 'pad';
9
9
  const IDIOM_TV = 'tv';
@@ -39,13 +39,13 @@ export const Platform = {
39
39
  return idiomEquals(IDIOM_VISION);
40
40
  },
41
41
  get isTesting() {
42
- return resolveConstants()?.isTesting ?? false;
42
+ return gatedIsTesting(resolveConstants());
43
43
  },
44
44
  // RN: isDisableAnimations ?? isTesting. The native flag wins; absent, it tracks
45
45
  // isTesting (test runs disable animations by default).
46
46
  get isDisableAnimations() {
47
47
  const constants = resolveConstants();
48
- return constants?.isDisableAnimations ?? constants?.isTesting ?? false;
48
+ return constants?.isDisableAnimations ?? gatedIsTesting(constants);
49
49
  },
50
50
  get isMacCatalyst() {
51
51
  return resolveConstants()?.isMacCatalyst ?? false;
@@ -23,3 +23,7 @@ export interface IPlatformStatic<TConstants> {
23
23
  select<T>(spec: IPlatformSelectSpec<T>): T | undefined;
24
24
  }
25
25
  export declare function createConstantsResolver<TConstants>(guard: (value: unknown) => value is TConstants): () => TConstants | undefined;
26
+ export declare function isDevBuild(): boolean;
27
+ export declare function gatedIsTesting(constants: {
28
+ isTesting?: boolean;
29
+ } | undefined): boolean;
@@ -38,3 +38,12 @@ export function createConstantsResolver(guard) {
38
38
  return cached;
39
39
  };
40
40
  }
41
+ // Metro's `__DEV__` global: `true` in a dev bundle, `false` in release. Read off globalThis so a
42
+ // host that never defines it (headless tooling) counts as release, like any unset flag.
43
+ export function isDevBuild() {
44
+ return Reflect.get(globalThis, '__DEV__') === true;
45
+ }
46
+ // RN's `isTesting`: the native flag in a dev build only (`if (__DEV__) … return false`).
47
+ export function gatedIsTesting(constants) {
48
+ return isDevBuild() && (constants?.isTesting ?? false);
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { IDynamicColorIOSTuple, IOpaqueColorValue } from './shared';
2
+ export * from './shared';
3
+ export declare function PlatformColor(...names: string[]): IOpaqueColorValue;
4
+ export declare function DynamicColorIOS(_tuple: IDynamicColorIOSTuple): IOpaqueColorValue;
@@ -0,0 +1,7 @@
1
+ export * from './shared.js';
2
+ export function PlatformColor(...names) {
3
+ return { resource_paths: names };
4
+ }
5
+ export function DynamicColorIOS(_tuple) {
6
+ throw new Error('DynamicColorIOS is not available on this platform.');
7
+ }
@@ -1,22 +1,4 @@
1
- export interface IDynamicColorIOSTuple {
2
- light: IColorValue;
3
- dark: IColorValue;
4
- highContrastLight?: IColorValue;
5
- highContrastDark?: IColorValue;
6
- }
7
- export interface IOpaqueColorValue {
8
- readonly semantic?: readonly string[];
9
- readonly dynamic?: {
10
- readonly light: IColorValue;
11
- readonly dark: IColorValue;
12
- readonly highContrastLight?: IColorValue;
13
- readonly highContrastDark?: IColorValue;
14
- };
15
- }
16
- export type IColorValue = string | number | IOpaqueColorValue;
1
+ import type { IDynamicColorIOSTuple, IOpaqueColorValue } from './shared';
2
+ export * from './shared';
17
3
  export declare function PlatformColor(...names: string[]): IOpaqueColorValue;
18
4
  export declare function DynamicColorIOS(tuple: IDynamicColorIOSTuple): IOpaqueColorValue;
19
- export declare function isOpaqueColorValue(value: unknown): value is IOpaqueColorValue;
20
- export declare function setColorProcessor(process: (value: IColorValue) => unknown): void;
21
- export declare function processColor(color: IColorValue): unknown;
22
- export declare function isProcessableColor(value: unknown): value is IColorValue;
@@ -1,10 +1,4 @@
1
- // PlatformColor / DynamicColorIOS: opaque color values the native side resolves
2
- // (iOS UIColor: semantic system colors and appearance-aware light/dark tuples).
3
- // These are pure object constructors with no native dependency, so they live in
4
- // shared and every adapter re-exports them. The opaque shape ({ semantic } /
5
- // { dynamic }) is exactly what RN's processColor and the iOS RCTConvert UIColor
6
- // path read, so our commit-time color seam routes these objects through the
7
- // injected processor just like CSS-string colors (see commit.ts processValue).
1
+ export * from './shared.js';
8
2
  export function PlatformColor(...names) {
9
3
  return { semantic: names };
10
4
  }
@@ -18,41 +12,3 @@ export function DynamicColorIOS(tuple) {
18
12
  },
19
13
  };
20
14
  }
21
- // True for the opaque objects above: the color seam uses this to route them
22
- // through the platform processor alongside CSS-string colors.
23
- export function isOpaqueColorValue(value) {
24
- return (typeof value === 'object' &&
25
- value !== null &&
26
- ('semantic' in value || 'dynamic' in value));
27
- }
28
- // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++ color
29
- // parser silently drops strings. The actual conversion (processColor) is RN-platform-specific,
30
- // so it is injected here rather than imported, keeping shared free of a react-native dependency
31
- // (and the headless harness working). This module is the sole owner of "run a value through the
32
- // platform color processor" - every color-touching consumer (commit's fabricProps,
33
- // process-box-shadow, process-filter, process-background-image, StatusBar android) imports
34
- // processColor/isProcessableColor from here, never from commit.ts.
35
- let colorProcessor = value => value;
36
- export function setColorProcessor(process) {
37
- colorProcessor = process;
38
- }
39
- // Public mirror of RN's processColor: run a color through the injected platform processor (the
40
- // canary wires RN's own). Off a real host it resolves CSS strings and opaque PlatformColor
41
- // objects to the platform ints Fabric expects; headless (no processor wired) it is the identity,
42
- // so smokes see the input unchanged.
43
- export function processColor(color) {
44
- return colorProcessor(color);
45
- }
46
- // A color-keyed value the platform processor must convert before Fabric: a CSS string, a numeric
47
- // literal, or an opaque PlatformColor / DynamicColorIOS object. `undefined` is left untouched.
48
- //
49
- // A number used to be excluded here on the reasoning that it was "already a resolved platform
50
- // int". Nothing in the pipeline produces one: the CSS parser emits hex STRINGS, and fabricProps
51
- // builds a fresh payload object instead of writing the resolved value back into node.props, so a
52
- // number reaching this guard is always the author's own literal. Excluding it committed
53
- // `color: 0xff0000ff` (opaque red, rrggbbaa) unrotated, and native reads that as aarrggbb: blue.
54
- export function isProcessableColor(value) {
55
- return (typeof value === 'string' ||
56
- typeof value === 'number' ||
57
- isOpaqueColorValue(value));
58
- }
@@ -0,0 +1,21 @@
1
+ export interface IDynamicColorIOSTuple {
2
+ light: IColorValue;
3
+ dark: IColorValue;
4
+ highContrastLight?: IColorValue;
5
+ highContrastDark?: IColorValue;
6
+ }
7
+ export interface IOpaqueColorValue {
8
+ readonly semantic?: readonly string[];
9
+ readonly resource_paths?: readonly string[];
10
+ readonly dynamic?: {
11
+ readonly light: IColorValue;
12
+ readonly dark: IColorValue;
13
+ readonly highContrastLight?: IColorValue;
14
+ readonly highContrastDark?: IColorValue;
15
+ };
16
+ }
17
+ export type IColorValue = string | number | IOpaqueColorValue;
18
+ export declare function isOpaqueColorValue(value: unknown): value is IOpaqueColorValue;
19
+ export declare function setColorProcessor(process: (value: IColorValue) => unknown): void;
20
+ export declare function processColor(color: IColorValue): unknown;
21
+ export declare function isProcessableColor(value: unknown): value is IColorValue;
@@ -0,0 +1,41 @@
1
+ // PlatformColor / DynamicColorIOS: opaque colors native resolves. Constructors are per platform
2
+ // because the shapes differ: iOS reads { semantic } / { dynamic } (PlatformColorParser.mm),
3
+ // Android only { resource_paths } (ColorPropConverter). The C++ seam forwards the object as-is.
4
+ // True for the opaque objects above: the color seam uses this to route them
5
+ // through the platform processor alongside CSS-string colors.
6
+ export function isOpaqueColorValue(value) {
7
+ return (typeof value === 'object' &&
8
+ value !== null &&
9
+ ('semantic' in value || 'dynamic' in value || 'resource_paths' in value));
10
+ }
11
+ // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++ color
12
+ // parser silently drops strings. The actual conversion (processColor) is RN-platform-specific,
13
+ // so it is injected here rather than imported, keeping shared free of a react-native dependency
14
+ // (and the headless harness working). This module is the sole owner of "run a value through the
15
+ // platform color processor" - every color-touching consumer (commit's fabricProps,
16
+ // process-box-shadow, process-filter, process-background-image, StatusBar android) imports
17
+ // processColor/isProcessableColor from here, never from commit.ts.
18
+ let colorProcessor = value => value;
19
+ export function setColorProcessor(process) {
20
+ colorProcessor = process;
21
+ }
22
+ // Public mirror of RN's processColor: run a color through the injected platform processor (the
23
+ // canary wires RN's own). Off a real host it resolves CSS strings and opaque PlatformColor
24
+ // objects to the platform ints Fabric expects; headless (no processor wired) it is the identity,
25
+ // so smokes see the input unchanged.
26
+ export function processColor(color) {
27
+ return colorProcessor(color);
28
+ }
29
+ // A color-keyed value the platform processor must convert before Fabric: a CSS string, a numeric
30
+ // literal, or an opaque PlatformColor / DynamicColorIOS object. `undefined` is left untouched.
31
+ //
32
+ // A number used to be excluded here on the reasoning that it was "already a resolved platform
33
+ // int". Nothing in the pipeline produces one: the CSS parser emits hex STRINGS, and fabricProps
34
+ // builds a fresh payload object instead of writing the resolved value back into node.props, so a
35
+ // number reaching this guard is always the author's own literal. Excluding it committed
36
+ // `color: 0xff0000ff` (opaque red, rrggbbaa) unrotated, and native reads that as aarrggbb: blue.
37
+ export function isProcessableColor(value) {
38
+ return (typeof value === 'string' ||
39
+ typeof value === 'number' ||
40
+ isOpaqueColorValue(value));
41
+ }
@@ -1,11 +1,6 @@
1
- // Post-commit hooks: run once after every native commit that changed the tree (after
2
- // completeRoot, when fresh Fabric tags have just been assigned). The seam exists so a
3
- // consumer that needs a node's committed tag — today only the Animated native driver,
4
- // which binds a props node to a view tag — can retry work that ran too early under an
5
- // async-batched commit (Vue/Svelte schedule completeRoot on a microtask, so lifecycle
6
- // code can reach a node before its tag exists; React commits synchronously and never
7
- // queues). A neutral module so commit.ts and animated/props.ts share it without a
8
- // dependency cycle.
1
+ // Run once after every commit that changed the tree, once fresh Fabric tags exist — lets a
2
+ // consumer needing a node's committed tag (the Animated native driver) retry work that ran too
3
+ // early under an async-batched commit. Shared by commit.ts and animated/props.ts, cycle-free.
9
4
  const hooks = new Set();
10
5
  export function registerPostCommit(hook) {
11
6
  hooks.add(hook);
@@ -1,10 +1,6 @@
1
- // JS-side port of RN's processAspectRatio (Libraries/StyleSheet/processAspectRatio.js).
2
- // Same root cause family as boxShadow/filter: RN registers `aspectRatio` with a JS
3
- // `process` because enableNativeCSSParsing() defaults to false, so the CSS ratio string
4
- // ('16 / 9') is resolved to a number IN JS before native. A plain number (the common,
5
- // already-working form) passes through untouched; this is a no-op for it.
6
- //
7
- // RN throws via invariant() in __DEV__ on a malformed value; we dlog and return undefined.
1
+ // JS-side port of RN's processAspectRatio: resolves a CSS ratio string ('16 / 9') to a number
2
+ // before native, same root cause as boxShadow/filter. A plain number passes through untouched.
3
+ // RN throws via invariant() on a malformed value; here we dlog and return undefined instead.
8
4
  import { dlog } from './debug.js';
9
5
  // RN processAspectRatio.js:15-63. number → number; ratio string → number; invalid → undefined.
10
6
  export function processAspectRatio(aspectRatio) {
@@ -1,20 +1,12 @@
1
1
  // RN's own processBackgroundSize / processBackgroundPosition / processBackgroundRepeat, imported
2
2
  // rather than ported.
3
- //
4
- // Why they are needed at all: ReactNativeStyleAttributes registers all three with
5
- // `nativeCSSParsing ? true : {process: processX}` (:50, :54, :58), and enableNativeCSSParsing()
6
- // defaults to false - so RN's stock path parses the CSS string into the per-axis structure
7
- // Fabric's C++ expects. We registered `experimental_backgroundImage` and not its three siblings,
8
- // so a string for any of them reached native unparsed: dropped on iOS, cast to the wrong
9
- // ReadableMap type on Android. Same shape as the process-transform incident.
10
- //
11
- // Why importing is free here even by this repo's strictest reading: all three upstream files
12
- // contain ONLY `import type` lines. Their value-import closure is empty - no processColor, no
13
- // Platform, no TurboModule floor, no renderer. There is nothing to weigh.
14
- //
15
- // Why no try/catch, unlike the other upstream wrappers: these three do not validate through
16
- // `invariant`. They answer `[]` for anything they cannot parse, which upstream's own comment calls
17
- // "do not apply any background" - so refusal is a RETURN here, not a throw.
3
+ // Needed because enableNativeCSSParsing() defaults to false, so RN's stock JS path parses the CSS
4
+ // string into the per-axis structure Fabric's C++ expects. Without it a string for any of these
5
+ // three reached native unparsed: dropped on iOS, cast to the wrong ReadableMap type on Android.
6
+ // Importing is free here: all three upstream files contain only `import type` lines, so their
7
+ // value-import closure is empty — no processColor, no Platform, no TurboModule floor, no renderer.
8
+ // No try/catch, unlike the other upstream wrappers: these three don't validate through invariant,
9
+ // they answer `[]` for anything unparseable — so refusal is a return here, not a throw.
18
10
  import { dlog } from './debug.js';
19
11
  // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
20
12
  import processBackgroundSizeUpstream from 'react-native/Libraries/StyleSheet/processBackgroundSize';
@@ -22,10 +14,9 @@ import processBackgroundSizeUpstream from 'react-native/Libraries/StyleSheet/pro
22
14
  import processBackgroundPositionUpstream from 'react-native/Libraries/StyleSheet/processBackgroundPosition';
23
15
  // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
24
16
  import processBackgroundRepeatUpstream from 'react-native/Libraries/StyleSheet/processBackgroundRepeat';
25
- // An empty result is upstream's way of saying it parsed nothing usable. The payload builder's
26
- // rule is that a refused processor leaves the key ABSENT, so it must not become `[]` - an empty
27
- // array is a real value, and committing one asks native to apply "no background size" rather than
28
- // leaving the declaration unset.
17
+ // An empty result means upstream parsed nothing usable. The payload builder's rule is that a
18
+ // refused processor leaves the key absent — `[]` is a real value, committing it would ask native
19
+ // to apply "no background size" rather than leave the declaration unset.
29
20
  function orAbsent(parsed, key, value) {
30
21
  if (!Array.isArray(parsed) || parsed.length === 0) {
31
22
  dlog(`${key}: RN parsed nothing usable from ${JSON.stringify(value)}`);
@@ -1,22 +1,14 @@
1
- // RN's own processFilter, imported rather than ported.
2
- //
3
- // Why it is needed at all: RN registers `filter` with `nativeCSSParsing ? true : {process:
4
- // processFilter}`, and enableNativeCSSParsing() defaults to false - so the stock path parses the
5
- // CSS string / structured array in JS and native never sees the raw string.
6
- //
7
- // Why the try/catch, unlike processBoxShadow's wrapper: upstream throws exactly once
8
- // (processFilter.js:120, a TypeError for an input that is neither string nor array), and a commit
9
- // path must never throw. fabric-props narrows before calling, but `processFilter` is on the public
10
- // barrel, so the guard is reachable rather than ceremonial.
11
- //
12
- // What changed by importing: a drop-shadow's colour now goes through RN's own `processColor`
13
- // (upstream imports it directly at processFilter.js:16) instead of our injected
14
- // `setColorProcessor` seam. On a real host those are the same function - bootstrapHost injects
15
- // exactly `processColor` - so the seam simply no longer governs the colour inside a filter.
16
- //
17
- // Platform caveat worth keeping in view when demoing this: on iOS RN paints only `brightness` and
18
- // `opacity` unless enableSwiftUIBasedFilters is on, so grayscale/blur/saturate/contrast/hueRotate
19
- // parse correctly here and still do nothing on screen.
1
+ // RN's own processFilter, imported rather than ported: enableNativeCSSParsing() defaults to
2
+ // false, so the stock path parses the CSS string / structured array in JS and native never sees
3
+ // the raw string.
4
+ // try/catch because upstream throws once (a TypeError for a non-string/array input) and a commit
5
+ // path must never throw; fabric-props narrows first, but processFilter is on the public barrel
6
+ // too, so the guard here stays reachable, not ceremonial.
7
+ // A drop-shadow's color goes through RN's own processColor here, not our injected
8
+ // setColorProcessor seam — the same function on a real host, since bootstrapHost injects exactly
9
+ // processColor, so the seam simply doesn't govern a filter's color.
10
+ // iOS caveat: RN paints only brightness/opacity unless enableSwiftUIBasedFilters is on, so
11
+ // grayscale/blur/saturate/contrast/hueRotate parse fine here and still paint nothing on screen.
20
12
  import { dlog } from './debug.js';
21
13
  // @ts-expect-error - untyped Flow source. Metro compiles it; vitest.config.ts strips the types.
22
14
  import processFilterUpstream from 'react-native/Libraries/StyleSheet/processFilter';
@@ -1,10 +1,6 @@
1
- // JS-side port of RN's processFontVariant (Libraries/StyleSheet/processFontVariant.js).
2
- // Same root cause family as boxShadow/filter: RN registers `fontVariant` with a JS
3
- // `process` because enableNativeCSSParsing() defaults to false, so a space-separated CSS
4
- // string ('small-caps tabular-nums') is split into the array native expects IN JS. An
5
- // array (the common, already-working form) passes through untouched; this is a no-op
6
- // for it.
7
- // RN processFontVariant.js:15-28. Array → array; space-separated string → array of variants.
1
+ // JS-side port of RN's processFontVariant: splits a space-separated CSS string
2
+ // ('small-caps tabular-nums') into the array native expects, same root cause as
3
+ // boxShadow/filter. An array (the common, already-working form) passes through as a no-op.
8
4
  export function processFontVariant(fontVariant) {
9
5
  if (Array.isArray(fontVariant)) {
10
6
  return fontVariant;
@@ -29,39 +29,6 @@ export declare function registerComponent(name: string, registration?: IComponen
29
29
  export declare function isRegisteredEvent(component: string, listener: string): boolean;
30
30
  export declare function registeredNativeEvent(component: string, raw: string): INativeEventBinding | undefined;
31
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
32
  export declare function configProcessedKeys(component: string): ReadonlySet<string> | undefined;
66
33
  export declare function configPayloadFold(component: string): IPayloadFold | undefined;
67
34
  export {};
package/build/registry.js CHANGED
@@ -1,33 +1,19 @@
1
- // Runtime metadata for native Fabric views, DERIVED BY DEFAULT. Any RN library
2
- // already ships its own ViewConfig: codegen registers it into RN's
3
- // ReactNativeViewConfigRegistry the moment the library's native-component module is
4
- // imported. That config carries everything the shared engine can't infer: which
5
- // events the view emits (bubblingEventTypes / directEventTypes) and how to process
6
- // its props (validAttributes[*].process, e.g. processColor). So we DON'T transcribe
7
- // any of it, and we don't mark anything "third-party": there is no
8
- // per-package registration to maintain. The engine reads the config for ANY
9
- // component on first use. Install a community view library, render it, done.
10
- //
11
- // shared must stay react-native-free (the headless harness runs in plain Node), so
12
- // the ViewConfig lookup is INJECTED, exactly like the color processor: the adapter
13
- // wires `setNativeViewConfigSource(ReactNativeViewConfigRegistry.get)` on a real
14
- // host, where that one source covers BOTH RN core and every library.
15
- //
16
- // The ONLY explicit list is OUR OWN built-in primitives (BUILTIN_COMPONENTS): a
17
- // finite set we own, which keep their hand-tuned tables (view-config events, commit
18
- // COLOR_PROPS) and are never read from the source, so they can't drift. Everything
19
- // NOT in that set derives. The list never grows with the community; it grows only
20
- // when we add a core primitive of our own.
1
+ // Runtime metadata for native Fabric views, derived by default. Any RN library ships its own
2
+ // ViewConfig via codegen, carrying everything the shared engine can't infer — which events a view
3
+ // emits, how to process its props. Nothing is transcribed; read from the config on first use.
4
+ // shared must stay react-native-free (the headless harness runs in plain Node), so the ViewConfig
5
+ // lookup is injected, exactly like the color processor: the adapter wires
6
+ // setNativeViewConfigSource(ReactNativeViewConfigRegistry.get) on a real host.
7
+ // The only explicit list is our own built-in primitives (BUILTIN_COMPONENTS): a finite set we own,
8
+ // which keep their hand-tuned tables and are never read from the source, so they can't drift. It
9
+ // grows only when we add a core primitive of our own, never for a community package.
21
10
  import { isRecord } from './type-guards.js';
22
11
  const EMPTY = {
23
12
  listeners: new Set(),
24
13
  byRaw: new Map(),
25
14
  processors: new Map(),
26
15
  };
27
- // OUR own primitives: the finite set shared hand-tunes (view-config events,
28
- // commit COLOR_PROPS). The source is never consulted for these, so they can't
29
- // drift. Everything else derives. This list grows only when WE add a core
30
- // primitive, never for a community package.
16
+ // Our own primitives — see the module header for why the source is never consulted for these.
31
17
  const BUILTIN_COMPONENTS = new Set([
32
18
  'RCTView',
33
19
  'RCTText',
@@ -156,39 +142,18 @@ export function registeredNativeEvent(component, raw) {
156
142
  export function registeredProcessor(component, key) {
157
143
  return resolve(component).processors.get(key);
158
144
  }
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
- */
145
+ // A component's ViewConfig processors, as a payload fold — or undefined when it has none. Not a
146
+ // lookup at payload-build time: the payload is built in C++ on a device and this registry can't
147
+ // cross that boundary (it's populated lazily from an injected JS-closure-holding lookup).
148
+ // payloadFold is the one seam that already runs in JS on both paths, so putting the processors
149
+ // there is what makes a third-party view behave the same on device as in a test. The alternative,
150
+ // a list of prop names hardcoded in C++, needs editing native code for every new component we see.
151
+ // Resolved once per component and cached, since createElement asks per node. A built-in
152
+ // short-circuits inside resolve before any of this.
153
+ // The prop names a component's own ViewConfig claims a processor for — exactly the keys
154
+ // configPayloadFold has already converted by the time the payload builder's own passes run.
155
+ // Exists so "a colour is converted exactly once" is a stated rule, not a lucky one: an overlap
156
+ // between a third-party config and the engine's own COLOR_PROPS would otherwise double-convert.
192
157
  export function configProcessedKeys(component) {
193
158
  const cached = keysCache.get(component);
194
159
  if (cached !== undefined)
@@ -1,21 +1,8 @@
1
1
  // Hand an error to the host the way React Native itself does, for the seams where an adapter
2
2
  // catches something its framework would otherwise have surfaced on its own.
3
- //
4
- // RN routes an uncaught render error through ReactFiberErrorDialog -> ExceptionsManager
5
- // (Libraries/Core/ReactFiberErrorDialog.js), which is reachable only by a deep path into RN's
6
- // Libraries. `global.ErrorUtils` is the same surface through the documented global that
7
- // @react-native/js-polyfills installs before any module evaluates - RN's own
8
- // Libraries/vendor/core/ErrorUtils.js is nothing but `export default global.ErrorUtils`. Off a
9
- // native host (vitest, node smokes) the global is absent and console.error is the whole channel.
10
- //
11
- // This is NOT the dlog channel. dlog is DEBUG-gated and therefore the developer's; an error that
12
- // blanked the screen has to reach the app whether or not anyone turned diagnostics on.
13
- // Read off globalThis through Reflect rather than named as an identifier, and deliberately NOT
14
- // declared in an ambient block: react-native ships its own `const ErrorUtils: ErrorUtils` in
15
- // src/types/globals.d.ts, so a `declare global { var ErrorUtils }` here redeclares it (TS2451) for
16
- // every package whose tsconfig pulls in RN's types - the engine's own does not, so it typechecks
17
- // clean here and breaks ngc downstream. RN's declared shape also lists only the two
18
- // get/setGlobalHandler members, not reportError, which the polyfill does install.
3
+ // Read off globalThis via Reflect, not a named identifier or an ambient declare: react-native
4
+ // ships its own `ErrorUtils` global type, so redeclaring it here breaks any package whose tsconfig
5
+ // pulls in RN's types. RN's declared shape also omits reportError, which the polyfill installs.
19
6
  function nativeReporter() {
20
7
  const utils = Reflect.get(globalThis, 'ErrorUtils');
21
8
  if (typeof utils !== 'object' || utils === null)
@@ -54,8 +41,7 @@ export function reportUncaughtError(value, info) {
54
41
  const report = nativeReporter();
55
42
  if (report !== null) {
56
43
  // Exactly one channel, never both: RN routes console.error into LogBox as well, so logging
57
- // here too would double-report every error. Upstream returns false from showErrorDialog for
58
- // this same reason (Libraries/Core/ReactFiberErrorDialog.js:57).
44
+ // here too would double-report every error.
59
45
  report(error);
60
46
  return;
61
47
  }
@@ -0,0 +1,6 @@
1
+ export declare const Settings: {
2
+ get(_key: string): unknown;
3
+ set(_settings: Record<string, unknown>): void;
4
+ watchKeys(_keys: string | string[], _callback: () => void): number;
5
+ clearWatch(_watchId: number): void;
6
+ };
@@ -0,0 +1,21 @@
1
+ // Settings, Android build: RN's SettingsFallback (Settings.js resolves to it off iOS). Android has
2
+ // no SettingsManager, so every call warns and does nothing — no JS snapshot, no watchers. The
3
+ // warning is RN's own user-facing one, not a diagnostic log.
4
+ const UNSUPPORTED = 'Settings is not yet supported on this platform.';
5
+ const NO_WATCH_ID = -1;
6
+ export const Settings = {
7
+ get(_key) {
8
+ console.warn(UNSUPPORTED);
9
+ return null;
10
+ },
11
+ set(_settings) {
12
+ console.warn(UNSUPPORTED);
13
+ },
14
+ watchKeys(_keys, _callback) {
15
+ console.warn(UNSUPPORTED);
16
+ return NO_WATCH_ID;
17
+ },
18
+ clearWatch(_watchId) {
19
+ console.warn(UNSUPPORTED);
20
+ },
21
+ };
@@ -1,8 +1 @@
1
- declare class SettingsImpl {
2
- get(key: string): unknown;
3
- set(settings: Record<string, unknown>): void;
4
- watchKeys(keys: string | string[], callback: () => void): number;
5
- clearWatch(watchId: number): void;
6
- }
7
- export declare const Settings: SettingsImpl;
8
- export {};
1
+ export * from './index.ios';
@@ -0,0 +1,8 @@
1
+ declare class SettingsImpl {
2
+ get(key: string): unknown;
3
+ set(settings: Record<string, unknown>): void;
4
+ watchKeys(keys: string | string[], callback: () => void): number;
5
+ clearWatch(watchId: number): void;
6
+ }
7
+ export declare const Settings: SettingsImpl;
8
+ export {};