@symbiote-native/engine 0.1.7 → 0.2.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/build/alert/index.ios.js +10 -0
- package/build/animated/color.d.ts +2 -8
- package/build/animated/color.js +4 -45
- package/build/animated/easing.js +0 -8
- package/build/animated/graph.d.ts +13 -5
- package/build/animated/graph.js +85 -11
- package/build/animated/index.d.ts +2 -2
- package/build/animated/index.js +2 -2
- package/build/animated/interpolation.js +1 -1
- package/build/animated/leaf-lifecycle.d.ts +7 -0
- package/build/animated/leaf-lifecycle.js +185 -0
- package/build/animated/native/native-animated.js +1 -0
- package/build/animated/rgba.d.ts +8 -0
- package/build/animated/rgba.js +55 -0
- package/build/animated/value.js +12 -7
- package/build/commit.js +1 -5
- package/build/debug.d.ts +1 -1
- package/build/debug.js +10 -3
- package/build/events/index.js +0 -2
- package/build/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/layout-animation/index.js +0 -1
- package/build/process-font-variant.js +0 -1
- package/build/process-transform/index.js +13 -19
- package/build/report-error.d.ts +5 -0
- package/build/report-error.js +60 -0
- package/build/status-bar/index.android.js +5 -6
- package/build/style-registry/index.js +95 -16
- package/build/style-sheet/index.js +4 -2
- package/build/surface.js +2 -1
- package/build/vibration/index.android.js +2 -3
- package/package.json +1 -1
- package/build/animated/interpolation-node.d.ts +0 -15
- package/build/animated/interpolation-node.js +0 -58
- package/build/style-registry/scope/index.d.ts +0 -3
- package/build/style-registry/scope/index.js +0 -41
- package/build/touch-history/index.d.ts +0 -23
- package/build/touch-history/index.js +0 -150
- package/build/type-guards/index.d.ts +0 -4
- package/build/type-guards/index.js +0 -15
package/build/animated/value.js
CHANGED
|
@@ -119,13 +119,12 @@ export class AnimatedValue extends AnimatedWithChildren {
|
|
|
119
119
|
this.offset = offset;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
//
|
|
122
|
+
// Owns the native-driver "start" handshake end-to-end: make this value native, mint
|
|
123
123
|
// its tag, hand the curve to the native module, and on completion sync the JS value
|
|
124
|
-
// (no re-flush
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// Information Expert: this value is the one object that actually holds those internals.
|
|
124
|
+
// (no re-flush; native already moved the view). The driver (BaseAnimation.
|
|
125
|
+
// startNativeIfNeeded) supplies only what it owns - curve config, animation id,
|
|
126
|
+
// platform bag, end callback - never touching __makeNative / __getNativeTag /
|
|
127
|
+
// __onNativeUpdate / flushValue directly; those internals live here.
|
|
129
128
|
__startNativeAnimation(config, nativeId, onEnd, platformConfig) {
|
|
130
129
|
this.__makeNative(platformConfig);
|
|
131
130
|
nativeAnimated.startAnimatingNode(nativeId, this.__getNativeTag(), config, result => {
|
|
@@ -164,10 +163,16 @@ export class AnimatedValue extends AnimatedWithChildren {
|
|
|
164
163
|
this.tracking = null;
|
|
165
164
|
}
|
|
166
165
|
}
|
|
167
|
-
// Stop any animation and reset to the original value.
|
|
166
|
+
// Stop any animation and reset to the original value. The native graph keeps its OWN copy of
|
|
167
|
+
// the value, so a native-driven node has to be told as well - otherwise the reset lands in JS
|
|
168
|
+
// only and the view stays wherever the animation left it. setValue above pushes for the same
|
|
169
|
+
// reason; RN pushes here too (Libraries/Animated/nodes/AnimatedValue.js:285).
|
|
168
170
|
resetAnimation(callback) {
|
|
169
171
|
this.stopAnimation(callback);
|
|
170
172
|
this.value = this.startingValue;
|
|
173
|
+
if (this.isNative) {
|
|
174
|
+
nativeAnimated.setAnimatedNodeValue(this.__getNativeTag(), this.startingValue);
|
|
175
|
+
}
|
|
171
176
|
}
|
|
172
177
|
// Drive this value with an animation. Typically called by Animated.timing /
|
|
173
178
|
// spring / decay rather than directly.
|
package/build/commit.js
CHANGED
|
@@ -20,9 +20,7 @@ import { nextTag } from './tags.js';
|
|
|
20
20
|
import { registerPostCommit, runPostCommitHooks } from './post-commit.js';
|
|
21
21
|
import { fabricProps } from './fabric-props.js';
|
|
22
22
|
import { isRecord } from './type-guards.js';
|
|
23
|
-
//
|
|
24
|
-
// leaf every color-touching module imports from); re-exported here so nothing outside this
|
|
25
|
-
// module needs to change its import path.
|
|
23
|
+
// Re-exported from ./platform-color so callers don't need to change their import path.
|
|
26
24
|
export { processColor, setColorProcessor } from './platform-color/index.js';
|
|
27
25
|
// Per-commit work counters, surfaced via dlog so a device run can prove the
|
|
28
26
|
// engine is incremental (created=0 with clones after the first mount).
|
|
@@ -173,8 +171,6 @@ function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, force
|
|
|
173
171
|
const props = fabricProps(node);
|
|
174
172
|
const childInText = node.isText || hasTextAncestor;
|
|
175
173
|
const committed = mirror.get(node);
|
|
176
|
-
// The children that actually reach Fabric. Anchors are filtered out here so the
|
|
177
|
-
// whole walk (child-set emission, identity diff, mirror) is anchor-blind.
|
|
178
174
|
const kids = renderableChildren(node);
|
|
179
175
|
// First mount, or the view kind flipped (RCTText <-> RCTVirtualText when a
|
|
180
176
|
// <Text> moves in or out of another <Text>): a different native component
|
package/build/debug.d.ts
CHANGED
package/build/debug.js
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
// Metro start (--reset-cache), not a rebuild.
|
|
6
6
|
// - runtime: globalThis.__SYMBIOTE_DEBUG__ = true, an escape hatch for hosts
|
|
7
7
|
// where the env isn't reachable.
|
|
8
|
-
// Production with neither set pays one property read per call and nothing else
|
|
8
|
+
// Production with neither set pays one property read per call and nothing else -
|
|
9
|
+
// but ONLY if the caller does not build the message itself first. A template
|
|
10
|
+
// literal is evaluated at the CALL SITE, before dlog can decide anything, so a
|
|
11
|
+
// `dlog(\`… ${JSON.stringify(x)}\`)` on a per-frame path costs its full price with
|
|
12
|
+
// logging off. On a hot path (a getter Angular re-reads every change-detection
|
|
13
|
+
// pass, an Animated reconcile, a scroll-driven apply) pass a THUNK instead:
|
|
14
|
+
// `dlog(() => \`…\`)` - it is only called once the switch is on.
|
|
9
15
|
function envEnabled() {
|
|
10
16
|
return typeof process !== 'undefined' && process.env.DEBUG === '1';
|
|
11
17
|
}
|
|
@@ -13,6 +19,7 @@ export function isDebug() {
|
|
|
13
19
|
return globalThis.__SYMBIOTE_DEBUG__ === true || envEnabled();
|
|
14
20
|
}
|
|
15
21
|
export function dlog(message) {
|
|
16
|
-
if (isDebug())
|
|
17
|
-
|
|
22
|
+
if (!isDebug())
|
|
23
|
+
return;
|
|
24
|
+
console.log(`[symbiote] ${typeof message === 'function' ? message() : message}`);
|
|
18
25
|
}
|
package/build/events/index.js
CHANGED
|
@@ -515,7 +515,6 @@ function bubble(target, listenerName, nativeEvent) {
|
|
|
515
515
|
while (node) {
|
|
516
516
|
const listener = isAnchor(node) ? undefined : node.listeners?.get(listenerName);
|
|
517
517
|
if (listener) {
|
|
518
|
-
// engine owner adds currentTarget + stopPropagation to SymbioteEvent
|
|
519
518
|
const event = {
|
|
520
519
|
type: listenerName,
|
|
521
520
|
target,
|
|
@@ -535,7 +534,6 @@ function deliverDirect(target, listenerName, nativeEvent) {
|
|
|
535
534
|
const listener = target.listeners?.get(listenerName);
|
|
536
535
|
if (!listener)
|
|
537
536
|
return;
|
|
538
|
-
// engine owner adds currentTarget + stopPropagation to SymbioteEvent
|
|
539
537
|
listener({
|
|
540
538
|
type: listenerName,
|
|
541
539
|
target,
|
package/build/index.d.ts
CHANGED
|
@@ -37,11 +37,13 @@ export type { IPlatformStatic, IPlatformOSType, IPlatformSelectSpec } from './pl
|
|
|
37
37
|
export type { IPlatformConstantsIOS } from './platform/index.ios';
|
|
38
38
|
export type { IPlatformConstantsAndroid } from './platform/index.android';
|
|
39
39
|
export { dlog, isDebug } from './debug';
|
|
40
|
+
export { reportUncaughtError } from './report-error';
|
|
41
|
+
export type { IUncaughtErrorInfo } from './report-error';
|
|
40
42
|
export { getNativeModule, getEnforcingNativeModule } from './native-modules';
|
|
41
43
|
export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource } from './native-events';
|
|
42
44
|
export type { IEventSubscription, IEventEmitterModule, INativeEventListener, IDeviceEventSource, } from './native-events';
|
|
43
|
-
export { AnimatedNode, AnimatedWithChildren, AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedInterpolation, AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, AnimatedEvent, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, flushValue, Easing, createNumericInterpolation, checkValidRanges, TimingAnimation, SpringAnimation, DecayAnimation, AnimatedTracking, timing, spring, decay, parallel, sequence, stagger, loop, delay, nativeAnimated, isNativeAnimatedAvailable, AnimatedProps, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated';
|
|
44
|
-
export type { IValueXY, IRgbaValue, IColorInput, IEventConfig, IEventListener, IAnimatedEventHandler, INativeEventAttachment, IValueListener, IEasingFunction, IInterpolationConfig, IExtrapolateType, IAnimation, IEndCallback, IEndResult, ICompositeAnimation, ITimingConfig, ISpringConfig, IDecayConfig, IParallelConfig, ILoopAnimationConfig, INativeNodeConfig, INativeAnimationConfig, INativeEventMapping, IPlatformConfig, } from './animated';
|
|
45
|
+
export { AnimatedNode, AnimatedWithChildren, AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedInterpolation, AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, AnimatedEvent, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, flushValue, Easing, createNumericInterpolation, checkValidRanges, TimingAnimation, SpringAnimation, DecayAnimation, AnimatedTracking, timing, spring, decay, parallel, sequence, stagger, loop, delay, nativeAnimated, isNativeAnimatedAvailable, AnimatedProps, createAnimatedLeafLifecycle, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated';
|
|
46
|
+
export type { IAnimatedLeafLifecycle, IValueXY, IRgbaValue, IColorInput, IEventConfig, IEventListener, IAnimatedEventHandler, INativeEventAttachment, IValueListener, IEasingFunction, IInterpolationConfig, IExtrapolateType, IAnimation, IEndCallback, IEndResult, ICompositeAnimation, ITimingConfig, ISpringConfig, IDecayConfig, IParallelConfig, ILoopAnimationConfig, INativeNodeConfig, INativeAnimationConfig, INativeEventMapping, IPlatformConfig, } from './animated';
|
|
45
47
|
export { getSlot } from './fabric';
|
|
46
48
|
export type { IFabricSlot, IFabricNode, IFabricChildSet, IFabricProps, IFabricEventHandler, IRootTag, IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, } from './fabric';
|
|
47
49
|
export { Alert } from './alert';
|
package/build/index.js
CHANGED
|
@@ -37,9 +37,10 @@ export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue,
|
|
|
37
37
|
export { scopeClassName } from './style-registry/scope.js';
|
|
38
38
|
export { Platform } from './platform';
|
|
39
39
|
export { dlog, isDebug } from './debug.js';
|
|
40
|
+
export { reportUncaughtError } from './report-error.js';
|
|
40
41
|
export { getNativeModule, getEnforcingNativeModule } from './native-modules/index.js';
|
|
41
42
|
export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource } from './native-events.js';
|
|
42
|
-
export { AnimatedNode, AnimatedWithChildren, AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedInterpolation, AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, AnimatedEvent, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, flushValue, Easing, createNumericInterpolation, checkValidRanges, TimingAnimation, SpringAnimation, DecayAnimation, AnimatedTracking, timing, spring, decay, parallel, sequence, stagger, loop, delay, nativeAnimated, isNativeAnimatedAvailable, AnimatedProps, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated/index.js';
|
|
43
|
+
export { AnimatedNode, AnimatedWithChildren, AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedInterpolation, AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, AnimatedEvent, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, flushValue, Easing, createNumericInterpolation, checkValidRanges, TimingAnimation, SpringAnimation, DecayAnimation, AnimatedTracking, timing, spring, decay, parallel, sequence, stagger, loop, delay, nativeAnimated, isNativeAnimatedAvailable, AnimatedProps, createAnimatedLeafLifecycle, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated/index.js';
|
|
43
44
|
export { getSlot } from './fabric.js';
|
|
44
45
|
// Imperative runtime modules: framework-agnostic native-bridge consumers (no visual, no
|
|
45
46
|
// lifecycle), moved here from @symbiote-native/react so every adapter re-exports the SAME module.
|
|
@@ -148,7 +148,6 @@ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
|
|
|
148
148
|
onAnimationDidEnd?.();
|
|
149
149
|
};
|
|
150
150
|
dlog(`LayoutAnimation.configureNext: dispatching config (duration=${config.duration})`);
|
|
151
|
-
// Default to a no-op when the caller doesn't supply one.
|
|
152
151
|
manager.configureNextLayoutAnimation(config, onComplete, onAnimationDidFail ?? (() => { }));
|
|
153
152
|
}
|
|
154
153
|
// ---- the LayoutAnimation facade ------------------------------------------
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
// JS-side port of RN's processTransform (Libraries/StyleSheet/processTransform.js).
|
|
2
|
-
// Same root cause as transformOrigin/boxShadow:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Android native cast to ReadableArray and crashed with
|
|
8
|
-
// `java.lang.String cannot be cast to com.facebook.react.bridge.ReadableArray`. This
|
|
9
|
-
// restores the missing JS parse so native always receives the entry array.
|
|
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.
|
|
10
7
|
//
|
|
11
|
-
// CRITICAL: the animated / sticky-header hot path produces transform ARRAYS
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// array unchanged (matching the boxShadow/transformOrigin idiom).
|
|
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.
|
|
17
13
|
import { dlog } from '../debug.js';
|
|
18
14
|
// RN processTransform.js:28: matches each `name(args)` in the CSS string.
|
|
19
15
|
const TRANSFORM_REGEX = /(\w+)\(([^)]+)\)/g;
|
|
@@ -78,16 +74,14 @@ function getKeyAndValueFromCSSTransform(key, args) {
|
|
|
78
74
|
return { key, value: isNaN(Number(args)) ? args : Number(args) };
|
|
79
75
|
}
|
|
80
76
|
}
|
|
81
|
-
// RN processTransform.js:24-50.
|
|
82
|
-
// is returned
|
|
83
|
-
// throws; we never throw, so we skip it and run a non-throwing dlog sanity check).
|
|
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).
|
|
84
79
|
export function processTransform(transform) {
|
|
85
80
|
if (transform == null) {
|
|
86
81
|
return [];
|
|
87
82
|
}
|
|
88
83
|
if (typeof transform !== 'string') {
|
|
89
|
-
// Hot path:
|
|
90
|
-
// same reference (no decompose, no clone) so the flush is a no-op.
|
|
84
|
+
// Hot path: return the same reference, no clone (see file header).
|
|
91
85
|
warnInvalidTransforms(transform);
|
|
92
86
|
return transform;
|
|
93
87
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Hand an error to the host the way React Native itself does, for the seams where an adapter
|
|
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.
|
|
19
|
+
function nativeReporter() {
|
|
20
|
+
const utils = Reflect.get(globalThis, 'ErrorUtils');
|
|
21
|
+
if (typeof utils !== 'object' || utils === null)
|
|
22
|
+
return null;
|
|
23
|
+
const report = Reflect.get(utils, 'reportError');
|
|
24
|
+
if (typeof report !== 'function')
|
|
25
|
+
return null;
|
|
26
|
+
return error => {
|
|
27
|
+
Reflect.apply(report, utils, [error]);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// A throw is not required to be an Error - a string, or null, reaches here just as easily, and
|
|
31
|
+
// the host's reporter wants something with a message and a stack. RN wraps the same way, via
|
|
32
|
+
// SyntheticError.
|
|
33
|
+
function toError(value, origin) {
|
|
34
|
+
if (value instanceof Error)
|
|
35
|
+
return value;
|
|
36
|
+
return new Error(`${origin}: ${String(value)}`);
|
|
37
|
+
}
|
|
38
|
+
export function reportUncaughtError(value, info) {
|
|
39
|
+
const error = toError(value, info.origin);
|
|
40
|
+
if (info.componentStack) {
|
|
41
|
+
// LogBox reads these two off the error to render the component frames; RN's own
|
|
42
|
+
// ReactFiberErrorDialog sets exactly the same pair, in a try/catch because a frozen or
|
|
43
|
+
// sealed error still deserves to be reported, just without the frames.
|
|
44
|
+
try {
|
|
45
|
+
Object.assign(error, { componentStack: info.componentStack, isComponentError: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Reported below regardless.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const report = nativeReporter();
|
|
52
|
+
if (report !== null) {
|
|
53
|
+
// Exactly one channel, never both: RN routes console.error into LogBox as well, so logging
|
|
54
|
+
// here too would double-report every error. Upstream returns false from showErrorDialog for
|
|
55
|
+
// this same reason (Libraries/Core/ReactFiberErrorDialog.js:57).
|
|
56
|
+
report(error);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.error(`[symbiote] ${info.origin}:`, error);
|
|
60
|
+
}
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
// setColor/setTranslucent, same module name ('StatusBarManager'). Metro picks this on an
|
|
5
5
|
// Android host; iOS keeps its own shape. Each adapter wraps these with its declarative component.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// light/dark text). See render.ts's installStopSurfaceGlobal.
|
|
7
|
+
// Driving window flags from our bridgeless surface used to blank the app: a status-bar
|
|
8
|
+
// relayout triggered stopSurface, which threw "Global was not installed" because RN's
|
|
9
|
+
// own renderer installs global.RN$stopSurface and we replace that renderer. Fixed by
|
|
10
|
+
// render.ts installing RN$stopSurface itself (see installStopSurfaceGlobal) - verified
|
|
11
|
+
// on device: show/hide + light/dark text survive the relayout without blanking.
|
|
13
12
|
import { getNativeModule } from '../native-modules/index.js';
|
|
14
13
|
import { dlog } from '../debug.js';
|
|
15
14
|
import { processColor } from '../platform-color/index.js';
|
|
@@ -11,11 +11,27 @@
|
|
|
11
11
|
// can write EITHER `class="sectionLabel"` OR `class="section-label"` - resolveOne below
|
|
12
12
|
// falls back to the kebab->camel form on a miss, since authors don't reliably write the
|
|
13
13
|
// exact camelCase key.
|
|
14
|
-
// Compound lookup tries every ordering of 2-4 space-separated class parts
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Compound lookup tries every ordering of 2-4 space-separated class parts (e.g. "btn primary"
|
|
15
|
+
// -> "btnPrimary" / "primaryBtn"), mirroring CSS compound-selector registration
|
|
16
|
+
// (`.btn.primary { }`), and layers whatever it finds ON TOP of the per-class merge.
|
|
17
17
|
const COMPOUND_MIN_PARTS = 2;
|
|
18
18
|
const COMPOUND_MAX_PARTS = 4;
|
|
19
|
+
// A scope suffix as the `<style scoped>` compilers emit it: `card__data-v-1a2b3c4d` (Vue) or
|
|
20
|
+
// `card__svelte-1a2b3c4d` (Svelte), the hash being css-parser's base36 hashFilePath. Matched by
|
|
21
|
+
// SHAPE, never by "there is a `__` somewhere in the name" — a BEM class (`card__title`) must
|
|
22
|
+
// never be read as a scoped `card`, which would silently merge the block's styles into the
|
|
23
|
+
// element's.
|
|
24
|
+
const SCOPE_SEPARATOR = '__';
|
|
25
|
+
const SCOPE_SUFFIX_PATTERN = /^(?:data-v|svelte)-[0-9a-z]+$/;
|
|
26
|
+
function splitScopedToken(token) {
|
|
27
|
+
const separator = token.lastIndexOf(SCOPE_SEPARATOR);
|
|
28
|
+
if (separator <= 0)
|
|
29
|
+
return null;
|
|
30
|
+
const scope = token.slice(separator + SCOPE_SEPARATOR.length);
|
|
31
|
+
if (!SCOPE_SUFFIX_PATTERN.test(scope))
|
|
32
|
+
return null;
|
|
33
|
+
return { base: token.slice(0, separator), scope };
|
|
34
|
+
}
|
|
19
35
|
const globalStyles = new Map();
|
|
20
36
|
// Called by generated code from side-effect style imports. Last import wins on a
|
|
21
37
|
// name collision, matching CSS cascade behavior.
|
|
@@ -49,18 +65,28 @@ export function resolveClassName(className) {
|
|
|
49
65
|
const trimmed = className.trim();
|
|
50
66
|
if (!trimmed)
|
|
51
67
|
return {};
|
|
52
|
-
const
|
|
68
|
+
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
69
|
+
// A single token is nothing but the exact-match lookup, so it goes through resolveOne, which
|
|
70
|
+
// adds the scoped-token base layering below. A multi-token string still tries the whole string
|
|
71
|
+
// as one key first — `$style.card`-style output arrives pre-resolved and must not be split.
|
|
72
|
+
if (parts.length <= 1)
|
|
73
|
+
return resolveOne(trimmed);
|
|
74
|
+
const exactMatch = lookupKey(trimmed);
|
|
53
75
|
if (exactMatch)
|
|
54
76
|
return exactMatch;
|
|
55
|
-
const
|
|
77
|
+
const merged = parts.reduce((acc, cls) => {
|
|
78
|
+
return { ...acc, ...resolveOne(cls) };
|
|
79
|
+
}, {});
|
|
80
|
+
// A compound rule LAYERS OVER the single-class rules rather than replacing them, matching the
|
|
81
|
+
// cascade: `.card { padding: 8; background: white }` + `.card.big { padding: 16 }` on
|
|
82
|
+
// `class="card big"` is padding 16 AND background white. Returning the compound alone (what
|
|
83
|
+
// this did before) silently dropped every property the compound did not itself restate.
|
|
56
84
|
if (parts.length >= COMPOUND_MIN_PARTS && parts.length <= COMPOUND_MAX_PARTS) {
|
|
57
85
|
const compound = tryCompoundLookup(parts);
|
|
58
86
|
if (compound)
|
|
59
|
-
return compound;
|
|
87
|
+
return { ...merged, ...compound };
|
|
60
88
|
}
|
|
61
|
-
return
|
|
62
|
-
return { ...acc, ...resolveOne(cls) };
|
|
63
|
-
}, {});
|
|
89
|
+
return merged;
|
|
64
90
|
}
|
|
65
91
|
function generateCompoundPermutations(parts) {
|
|
66
92
|
if (parts.length < COMPOUND_MIN_PARTS)
|
|
@@ -73,13 +99,13 @@ function generateCompoundPermutations(parts) {
|
|
|
73
99
|
}
|
|
74
100
|
function generateKPermutations(parts, size) {
|
|
75
101
|
if (size === 0)
|
|
76
|
-
return [
|
|
102
|
+
return [[]];
|
|
77
103
|
if (parts.length === 0)
|
|
78
104
|
return [];
|
|
79
105
|
const result = [];
|
|
80
106
|
function helper(current, remaining, depth) {
|
|
81
107
|
if (depth === size) {
|
|
82
|
-
result.push(
|
|
108
|
+
result.push(current);
|
|
83
109
|
return;
|
|
84
110
|
}
|
|
85
111
|
for (let i = 0; i < remaining.length; i++) {
|
|
@@ -113,16 +139,69 @@ export function kebabToCamel(value) {
|
|
|
113
139
|
function tryCompoundLookup(parts) {
|
|
114
140
|
if (parts.length < COMPOUND_MIN_PARTS)
|
|
115
141
|
return null;
|
|
116
|
-
for (const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
for (const subset of generateCompoundPermutations(parts)) {
|
|
143
|
+
for (const key of compoundKeysFor(subset)) {
|
|
144
|
+
const style = globalStyles.get(key);
|
|
145
|
+
if (style)
|
|
146
|
+
return style;
|
|
147
|
+
}
|
|
120
148
|
}
|
|
121
149
|
return null;
|
|
122
150
|
}
|
|
151
|
+
function compoundKeysFor(subset) {
|
|
152
|
+
const scoped = scopedCompoundKey(subset);
|
|
153
|
+
return scoped === null ? [toCompoundKey(subset)] : [toCompoundKey(subset), scoped];
|
|
154
|
+
}
|
|
155
|
+
// The scope suffix is appended per TOKEN in the markup (`class="card__svelte-h big__svelte-h"`)
|
|
156
|
+
// but appears ONCE, at the end, in the registered key a compound rule produces
|
|
157
|
+
// (`.card.big` -> `cardBig__svelte-h`) — the compiler collapses the selector to one name and
|
|
158
|
+
// suffixes that. Those two operations do not commute, so the key built from the raw tokens
|
|
159
|
+
// (`card__svelte-hBig__svelte-h`) can never match and every scoped compound rule was dead.
|
|
160
|
+
// Rebuild the key the way registration did: strip the suffix, join the bases, re-append once.
|
|
161
|
+
//
|
|
162
|
+
// An UNSCOPED token in the subset contributes its own name and no scope, rather than aborting.
|
|
163
|
+
// That is the `:global()` case, and it is why a partial `:global()` reaches the markup it was
|
|
164
|
+
// written for: `.card :global(.reset)` registers the key `cardReset`, which the scoper suffixes
|
|
165
|
+
// as a whole (the escape hatch exempts the `reset` MARKUP TOKEN, not the collapsed key), while
|
|
166
|
+
// the element carries `card__svelte-h reset`. The same shape covers a class handed down from a
|
|
167
|
+
// parent component, which likewise arrives unsuffixed.
|
|
168
|
+
//
|
|
169
|
+
// Widening, stated so it stays deliberate: a fully-scoped `.card.reset` collapses to that same
|
|
170
|
+
// key, so an element carrying a FOREIGN `reset` now matches a rule the author scoped to their
|
|
171
|
+
// own. The key format cannot tell the two apart — telling them apart needs a registry indexed by
|
|
172
|
+
// token set, with per-token scope. The scopes still have to agree: two tokens from two different
|
|
173
|
+
// components have no single suffix to factor out and stay unmatched.
|
|
174
|
+
function scopedCompoundKey(subset) {
|
|
175
|
+
const bases = [];
|
|
176
|
+
let scope;
|
|
177
|
+
for (const token of subset) {
|
|
178
|
+
const split = splitScopedToken(token);
|
|
179
|
+
if (split === null) {
|
|
180
|
+
bases.push(token);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (scope !== undefined && split.scope !== scope)
|
|
184
|
+
return null;
|
|
185
|
+
scope = split.scope;
|
|
186
|
+
bases.push(split.base);
|
|
187
|
+
}
|
|
188
|
+
return scope === undefined ? null : toCompoundKey(bases) + SCOPE_SEPARATOR + scope;
|
|
189
|
+
}
|
|
190
|
+
// A scoped token layers over its own unscoped name: on the web the element carries BOTH classes
|
|
191
|
+
// (`class="card svelte-h"`), so a global `.card` in App.css still applies underneath the
|
|
192
|
+
// component's `<style>` rule. Rewriting `card` -> `card__svelte-h` here is how the scope is
|
|
193
|
+
// expressed instead of a second class, so the base has to be re-consulted explicitly or that
|
|
194
|
+
// global rule silently disappears the moment a component defines a class of the same name.
|
|
123
195
|
function resolveOne(name) {
|
|
124
196
|
const trimmed = name.trim();
|
|
125
197
|
if (!trimmed)
|
|
126
198
|
return {};
|
|
127
|
-
|
|
199
|
+
const scoped = lookupKey(trimmed);
|
|
200
|
+
const split = splitScopedToken(trimmed);
|
|
201
|
+
if (split === null)
|
|
202
|
+
return scoped ?? {};
|
|
203
|
+
return { ...lookupKey(split.base), ...scoped };
|
|
204
|
+
}
|
|
205
|
+
function lookupKey(name) {
|
|
206
|
+
return globalStyles.get(name) ?? globalStyles.get(kebabToCamel(name));
|
|
128
207
|
}
|
|
@@ -49,8 +49,10 @@ const absoluteFill = Object.freeze({
|
|
|
49
49
|
top: 0,
|
|
50
50
|
bottom: 0,
|
|
51
51
|
});
|
|
52
|
-
// RN's composeStyles:
|
|
53
|
-
// (which flatten later collapses, later keys winning).
|
|
52
|
+
// RN's composeStyles: a null/undefined side yields the other, else the pair [a, b]
|
|
53
|
+
// (which flatten later collapses, later keys winning). The test is null/undefined, NOT
|
|
54
|
+
// falsy - compose(0, y) returns [0, y], matching react-native's own
|
|
55
|
+
// src/private/styles/composeStyles.js, which branches on `== null`.
|
|
54
56
|
function compose(style1, style2) {
|
|
55
57
|
if (style1 === null || style1 === undefined)
|
|
56
58
|
return style2;
|
package/build/surface.js
CHANGED
|
@@ -62,6 +62,7 @@ export class SymbioteSurface {
|
|
|
62
62
|
}
|
|
63
63
|
export function createSurface(rootTag) {
|
|
64
64
|
installEventHandler();
|
|
65
|
+
const surface = new SymbioteSurface(rootTag);
|
|
65
66
|
dlog(`surface created root=${rootTag}`);
|
|
66
|
-
return
|
|
67
|
+
return surface;
|
|
67
68
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Vibration: Android build. Android has a native pattern scheduler, so the whole pattern
|
|
2
|
-
// goes to native `vibrateByPattern(pattern, repeat)
|
|
3
|
-
//
|
|
4
|
-
// on an Android host.
|
|
2
|
+
// goes to native `vibrateByPattern(pattern, repeat)` (repeat encoding below). Everything
|
|
3
|
+
// else is the shared core. Metro picks this file on an Android host.
|
|
5
4
|
//
|
|
6
5
|
// device-verify-pending: the `Vibration` name and the `vibrateByPattern` routing are
|
|
7
6
|
// confirmed from RN source but not yet exercised on a real Android host: only a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "SymbioteNative's retained shadow-tree engine — clone-on-write commit path + event normalization over React Native Fabric, shared by every framework adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { AnimatedNode, AnimatedWithChildren } from './graph';
|
|
2
|
-
import { type IInterpolationConfig } from './interpolation';
|
|
3
|
-
import type { INativeNodeConfig, IPlatformConfig } from './native/native-animated';
|
|
4
|
-
export declare class AnimatedInterpolation extends AnimatedWithChildren {
|
|
5
|
-
private readonly parent;
|
|
6
|
-
private readonly config;
|
|
7
|
-
private interpolation;
|
|
8
|
-
constructor(parent: AnimatedNode, config: IInterpolationConfig);
|
|
9
|
-
private getInterpolation;
|
|
10
|
-
__getValue(): number | string;
|
|
11
|
-
__attach(): void;
|
|
12
|
-
__detach(): void;
|
|
13
|
-
__makeNative(platformConfig?: IPlatformConfig): void;
|
|
14
|
-
__getNativeConfig(): INativeNodeConfig;
|
|
15
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// AnimatedInterpolation: a graph node that maps its parent's numeric value
|
|
2
|
-
// through an interpolation. Ported from RN's AnimatedInterpolation.js: numeric,
|
|
3
|
-
// string-with-units, and color output ranges (the value graph; native config
|
|
4
|
-
// removed). Platform (Native) colors stay out of scope, color.ts defers them.
|
|
5
|
-
import { AnimatedNode, AnimatedWithChildren, registerInterpolationFactory } from './graph.js';
|
|
6
|
-
import { checkValidRanges, createInterpolation } from './interpolation.js';
|
|
7
|
-
export class AnimatedInterpolation extends AnimatedWithChildren {
|
|
8
|
-
parent;
|
|
9
|
-
config;
|
|
10
|
-
interpolation;
|
|
11
|
-
constructor(parent, config) {
|
|
12
|
-
super();
|
|
13
|
-
this.parent = parent;
|
|
14
|
-
this.config = config;
|
|
15
|
-
// Validate eagerly so a bad range fails at construction, not first frame.
|
|
16
|
-
checkValidRanges(config.inputRange, config.outputRange);
|
|
17
|
-
}
|
|
18
|
-
getInterpolation() {
|
|
19
|
-
if (this.interpolation === undefined) {
|
|
20
|
-
this.interpolation = createInterpolation(this.config);
|
|
21
|
-
}
|
|
22
|
-
return this.interpolation;
|
|
23
|
-
}
|
|
24
|
-
__getValue() {
|
|
25
|
-
const parentValue = this.parent.__getValue();
|
|
26
|
-
if (typeof parentValue !== 'number') {
|
|
27
|
-
throw new Error('Cannot interpolate an input which is not a number');
|
|
28
|
-
}
|
|
29
|
-
return this.getInterpolation()(parentValue);
|
|
30
|
-
}
|
|
31
|
-
__attach() {
|
|
32
|
-
this.parent.__addChild(this);
|
|
33
|
-
super.__attach();
|
|
34
|
-
}
|
|
35
|
-
__detach() {
|
|
36
|
-
this.parent.__removeChild(this);
|
|
37
|
-
super.__detach();
|
|
38
|
-
}
|
|
39
|
-
// Make the upstream value native first, so the parent->interpolation edge can be
|
|
40
|
-
// wired when this node is reached from a leaf rather than from the value.
|
|
41
|
-
__makeNative(platformConfig) {
|
|
42
|
-
this.parent.__makeNative(platformConfig);
|
|
43
|
-
super.__makeNative(platformConfig);
|
|
44
|
-
}
|
|
45
|
-
__getNativeConfig() {
|
|
46
|
-
return {
|
|
47
|
-
type: 'interpolation',
|
|
48
|
-
inputRange: this.config.inputRange,
|
|
49
|
-
outputRange: this.config.outputRange,
|
|
50
|
-
extrapolateLeft: this.config.extrapolateLeft ?? this.config.extrapolate ?? 'extend',
|
|
51
|
-
extrapolateRight: this.config.extrapolateRight ?? this.config.extrapolate ?? 'extend',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
// Hand graph.ts's AnimatedNode.interpolate() the real constructor at module load,
|
|
56
|
-
// so every node subclass's base-class interpolate() produces an actual
|
|
57
|
-
// AnimatedInterpolation without graph.ts ever value-importing this module.
|
|
58
|
-
registerInterpolationFactory((parent, config) => new AnimatedInterpolation(parent, config));
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export type IClassToggleMap = Record<string, boolean | undefined>;
|
|
2
|
-
export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
|
|
3
|
-
export declare function scopeClassName(value: IScopableClassValue, localNames: ReadonlySet<string>, scopeId: string): IScopableClassValue;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Vue `<style scoped>` class-name rewriter. Distinct responsibility from the sibling
|
|
2
|
-
// ./index.ts (the CSS class -> style registry): this module does pure NAME rewriting,
|
|
3
|
-
// no registry lookup, no CSS parsing. It runs at the compiled call site of a Vue SFC's
|
|
4
|
-
// scoped-style template - `adapters/vue/metro-vue-transformer.cjs` emits calls to
|
|
5
|
-
// scopeClassName (imported there as `__scopeClass`) - BEFORE Vue's own normalizeClass()
|
|
6
|
-
// collapses string/object/array `class` values to a final string, so it must pre-process
|
|
7
|
-
// all three shapes normalizeClass understands. resolveClassName in ./index.ts still does
|
|
8
|
-
// the actual style lookup, unchanged, against the rewritten (possibly suffixed) name.
|
|
9
|
-
import { kebabToCamel } from '../index.js';
|
|
10
|
-
// Suffixes every class token that this file's scoped block locally defines with
|
|
11
|
-
// `__${scopeId}`, leaving unrecognized tokens (globals, external classes) untouched.
|
|
12
|
-
export function scopeClassName(value, localNames, scopeId) {
|
|
13
|
-
if (value === undefined || value === null)
|
|
14
|
-
return value;
|
|
15
|
-
if (Array.isArray(value)) {
|
|
16
|
-
return value.map(item => scopeClassEntry(item, localNames, scopeId));
|
|
17
|
-
}
|
|
18
|
-
return scopeClassEntry(value, localNames, scopeId);
|
|
19
|
-
}
|
|
20
|
-
// A token arrives as either the camelCase registry key (`sectionLabel`) or its kebab-case
|
|
21
|
-
// authoring form (`section-label`) - normalize to camelCase FIRST, then decide scoping, so
|
|
22
|
-
// `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
|
|
23
|
-
// kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
|
|
24
|
-
function scopeToken(token, localNames, scopeId) {
|
|
25
|
-
const camelToken = kebabToCamel(token);
|
|
26
|
-
return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
|
|
27
|
-
}
|
|
28
|
-
function scopeClassEntry(value, localNames, scopeId) {
|
|
29
|
-
if (typeof value === 'object') {
|
|
30
|
-
const scoped = {};
|
|
31
|
-
for (const [name, enabled] of Object.entries(value)) {
|
|
32
|
-
scoped[scopeToken(name, localNames, scopeId)] = enabled;
|
|
33
|
-
}
|
|
34
|
-
return scoped;
|
|
35
|
-
}
|
|
36
|
-
return value
|
|
37
|
-
.split(/\s+/)
|
|
38
|
-
.filter(Boolean)
|
|
39
|
-
.map(token => scopeToken(token, localNames, scopeId))
|
|
40
|
-
.join(' ');
|
|
41
|
-
}
|