@symbiote-native/engine 0.1.4 → 0.1.6
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/accessibility-info/index.android.js +17 -29
- package/build/accessibility-info/index.ios.js +23 -37
- package/build/accessibility-info/shared.d.ts +3 -2
- package/build/accessibility-info/shared.js +25 -2
- package/build/action-sheet-ios/index.d.ts +6 -0
- package/build/action-sheet-ios/index.js +1 -1
- package/build/alert/index.android.js +1 -3
- package/build/animated/animations/base.js +5 -14
- package/build/animated/event.js +7 -9
- package/build/animated/graph.d.ts +6 -0
- package/build/animated/graph.js +14 -0
- package/build/animated/index.d.ts +1 -1
- package/build/animated/index.js +1 -1
- package/build/animated/interpolation-node.d.ts +0 -1
- package/build/animated/interpolation-node.js +5 -4
- package/build/animated/native/native-animated.js +1 -3
- package/build/animated/operators.d.ts +14 -34
- package/build/animated/operators.js +19 -95
- package/build/animated/value.d.ts +2 -4
- package/build/animated/value.js +17 -5
- package/build/app-state/index.js +17 -22
- package/build/appearance/index.js +17 -25
- package/build/back-handler/index.js +15 -18
- package/build/commit.d.ts +1 -3
- package/build/commit.js +26 -209
- package/build/dimensions/index.js +25 -20
- package/build/events/index.js +6 -151
- package/build/fabric-props.d.ts +3 -0
- package/build/fabric-props.js +186 -0
- package/build/i18n-manager/index.js +3 -5
- package/build/image-loader/index.d.ts +24 -0
- package/build/image-loader/index.js +175 -0
- package/build/image-loader.d.ts +24 -0
- package/build/image-loader.js +175 -0
- package/build/image-source-resolver/index.d.ts +9 -0
- package/build/image-source-resolver/index.js +16 -0
- package/build/image-source-resolver.d.ts +9 -0
- package/build/image-source-resolver.js +16 -0
- package/build/index.d.ts +10 -3
- package/build/index.js +12 -3
- package/build/keyboard/index.js +19 -34
- package/build/layout-animation/index.d.ts +1 -0
- package/build/layout-animation/index.js +53 -35
- package/build/native-modules/index.d.ts +18 -0
- package/build/native-modules/index.js +120 -0
- package/build/native-modules.d.ts +12 -0
- package/build/native-modules.js +64 -1
- package/build/node.d.ts +2 -0
- package/build/node.js +36 -1
- package/build/pan-responder/index.js +1 -3
- package/build/permissions-android/index.js +1 -3
- package/build/{platform-color.d.ts → platform-color/index.d.ts} +3 -0
- package/build/platform-color/index.js +49 -0
- package/build/process-background-image/index.js +7 -14
- package/build/process-box-shadow/index.js +1 -5
- package/build/process-filter.js +2 -8
- package/build/registry.js +3 -5
- package/build/settings/index.js +1 -3
- package/build/share/index.ios.js +1 -1
- package/build/status-bar/index.android.js +1 -1
- package/build/style/index.js +2 -4
- package/build/style-registry/index.d.ts +1 -3
- package/build/style-registry/index.js +15 -48
- package/build/style-registry/scope/index.d.ts +3 -0
- package/build/style-registry/scope/index.js +41 -0
- package/build/style-registry/scope.d.ts +3 -0
- package/build/style-registry/scope.js +41 -0
- package/build/touch-history/index.d.ts +23 -0
- package/build/touch-history/index.js +150 -0
- package/build/touch-history.d.ts +23 -0
- package/build/touch-history.js +150 -0
- package/build/type-guards/index.d.ts +4 -0
- package/build/type-guards/index.js +15 -0
- package/build/type-guards.d.ts +4 -0
- package/build/type-guards.js +15 -0
- package/package.json +1 -1
- package/build/platform-color.js +0 -25
- /package/build/animated/{animated-component-shared.d.ts → shared.d.ts} +0 -0
- /package/build/animated/{animated-component-shared.js → shared.js} +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Fabric-prop translation: turn a retained node's logical props into the flat payload
|
|
2
|
+
// Fabric's C++ props expect. Split out of commit.ts (which owns the reconciler/mirror walk
|
|
3
|
+
// + the imperative instance API, neither of which this half touches) so the two
|
|
4
|
+
// responsibilities stop sharing one 300+ line file. Color processing itself lives in
|
|
5
|
+
// ./platform-color (the stable leaf every color-touching module imports from); this file only
|
|
6
|
+
// decides WHICH props are color props and wires the structured CSS-style processors.
|
|
7
|
+
import { RAW_TEXT_COMPONENT } from './node.js';
|
|
8
|
+
import { flattenStyle } from './style/index.js';
|
|
9
|
+
import { registeredProcessor } from './registry.js';
|
|
10
|
+
import { isProcessableColor, processColor } from './platform-color/index.js';
|
|
11
|
+
import { processBoxShadow } from './process-box-shadow/index.js';
|
|
12
|
+
import { processFilter } from './process-filter.js';
|
|
13
|
+
import { processTransformOrigin } from './process-transform-origin/index.js';
|
|
14
|
+
import { processTransform } from './process-transform/index.js';
|
|
15
|
+
import { processAspectRatio } from './process-aspect-ratio.js';
|
|
16
|
+
import { processFontVariant } from './process-font-variant.js';
|
|
17
|
+
import { processBackgroundImage } from './process-background-image/index.js';
|
|
18
|
+
import { isRecord, isString } from './type-guards.js';
|
|
19
|
+
// Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
|
|
20
|
+
// color parser silently drops strings. The actual conversion (processColor) is
|
|
21
|
+
// RN-platform-specific, so it is injected in platform-color.ts rather than imported,
|
|
22
|
+
// keeping shared free of a react-native dependency (and the headless harness working).
|
|
23
|
+
const COLOR_PROPS = new Set([
|
|
24
|
+
'backgroundColor',
|
|
25
|
+
'color',
|
|
26
|
+
'borderColor',
|
|
27
|
+
'borderTopColor',
|
|
28
|
+
'borderRightColor',
|
|
29
|
+
'borderBottomColor',
|
|
30
|
+
'borderLeftColor',
|
|
31
|
+
// Logical (writing-direction-relative) border colors + the block axis, all wired to
|
|
32
|
+
// processColor in RN's ReactNativeStyleAttributes. borderStartColor/borderEndColor are
|
|
33
|
+
// even publicly typed ColorValue, so they silently dropped on iOS / threw on Android.
|
|
34
|
+
'borderStartColor',
|
|
35
|
+
'borderEndColor',
|
|
36
|
+
'borderBlockColor',
|
|
37
|
+
'borderBlockStartColor',
|
|
38
|
+
'borderBlockEndColor',
|
|
39
|
+
'shadowColor',
|
|
40
|
+
// Text shadow + the W3C `outline`/image `overlay` colors, also processColor in RN.
|
|
41
|
+
'textShadowColor',
|
|
42
|
+
'overlayColor',
|
|
43
|
+
'outlineColor',
|
|
44
|
+
'tintColor',
|
|
45
|
+
// TextInput color props. iOS's native input accepts a CSS string, but Android's
|
|
46
|
+
// AndroidTextInput is strict ("ColorValue: the value must be a number or Object"),
|
|
47
|
+
// so these must be processColor'd here too, same as any other color reaching Fabric.
|
|
48
|
+
'placeholderTextColor',
|
|
49
|
+
'selectionColor',
|
|
50
|
+
'cursorColor',
|
|
51
|
+
'underlineColorAndroid',
|
|
52
|
+
// Text decoration color (underline/strike): same Fabric strictness as any color.
|
|
53
|
+
'textDecorationColor',
|
|
54
|
+
'selectionHandleColor',
|
|
55
|
+
// Switch track/thumb colors. RN processColors each via the Switch ViewConfig
|
|
56
|
+
// (SwitchNativeComponent / AndroidSwitchNativeComponent validAttributes). iOS takes
|
|
57
|
+
// onTintColor (ON) / tintColor (OFF); Android takes trackColorForTrue/False +
|
|
58
|
+
// trackTintColor, and Android's ColorPropConverter is strict ("the value must be a
|
|
59
|
+
// number or Object"), so a raw CSS string crashes. thumbTintColor reaches both.
|
|
60
|
+
'onTintColor',
|
|
61
|
+
'thumbTintColor',
|
|
62
|
+
'trackColorForTrue',
|
|
63
|
+
'trackColorForFalse',
|
|
64
|
+
'trackTintColor',
|
|
65
|
+
]);
|
|
66
|
+
// Structured CSS-style keys RN parses in JS before native (boxShadow/filter register
|
|
67
|
+
// with enableNativeCSSParsing(), which DEFAULTS TO FALSE, so native CSS parsing is off
|
|
68
|
+
// and the raw string is dropped). Each runs on the hoisted top-level style key, turning
|
|
69
|
+
// a CSS string or structured array into the processed array Fabric's C++ expects.
|
|
70
|
+
const STYLE_PROCESSORS = new Map([
|
|
71
|
+
['boxShadow', value => processBoxShadow(asBoxShadowInput(value))],
|
|
72
|
+
['filter', value => processFilter(asFilterInput(value))],
|
|
73
|
+
['transformOrigin', value => processTransformOrigin(asTransformOriginInput(value))],
|
|
74
|
+
['transform', processTransformValue],
|
|
75
|
+
['aspectRatio', value => processAspectRatio(asAspectRatioInput(value))],
|
|
76
|
+
['fontVariant', value => processFontVariant(asFontVariantInput(value))],
|
|
77
|
+
['experimental_backgroundImage', value => processBackgroundImage(asBackgroundImageInput(value))],
|
|
78
|
+
]);
|
|
79
|
+
// boxShadow accepts a CSS string or an array of shadow objects; anything else is
|
|
80
|
+
// undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
|
|
81
|
+
function asBoxShadowInput(value) {
|
|
82
|
+
if (typeof value === 'string')
|
|
83
|
+
return value;
|
|
84
|
+
if (Array.isArray(value))
|
|
85
|
+
return value.filter(isRecord);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
// filter accepts a CSS string or an array of single-key filter objects; same narrowing.
|
|
89
|
+
function asFilterInput(value) {
|
|
90
|
+
if (typeof value === 'string')
|
|
91
|
+
return value;
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return value.filter(isRecord);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
// experimental_backgroundImage accepts a CSS string (gradient functions) or an array of
|
|
97
|
+
// structured gradient objects; same narrowing as boxShadow/filter.
|
|
98
|
+
function asBackgroundImageInput(value) {
|
|
99
|
+
if (typeof value === 'string')
|
|
100
|
+
return value;
|
|
101
|
+
if (Array.isArray(value))
|
|
102
|
+
return value.filter(isRecord);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
// transformOrigin accepts a CSS string or a [x, y, z] array of strings/numbers; anything
|
|
106
|
+
// else is undefined to processTransformOrigin (which defaults to center/center/0).
|
|
107
|
+
function asTransformOriginInput(value) {
|
|
108
|
+
if (typeof value === 'string')
|
|
109
|
+
return value;
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return value.filter(isStringOrNumber);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// aspectRatio accepts a number (the common, working form) or a ratio string; otherwise
|
|
115
|
+
// undefined, which processAspectRatio drops.
|
|
116
|
+
function asAspectRatioInput(value) {
|
|
117
|
+
if (typeof value === 'number' || typeof value === 'string')
|
|
118
|
+
return value;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
// fontVariant accepts an array of variant strings (the common, working form) or a
|
|
122
|
+
// space-separated string; anything else becomes an empty string, which yields [].
|
|
123
|
+
function asFontVariantInput(value) {
|
|
124
|
+
if (typeof value === 'string')
|
|
125
|
+
return value;
|
|
126
|
+
if (Array.isArray(value))
|
|
127
|
+
return value.filter(isString);
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
// transform accepts a CSS string (processTransform parses it) or an array of single-key
|
|
131
|
+
// transform records (the hot animated / sticky-header path, passed through unchanged).
|
|
132
|
+
// A non-string non-array value is NOT dropped: it may already be processed, so it passes
|
|
133
|
+
// through verbatim rather than being coerced to [] (which would erase a valid transform).
|
|
134
|
+
function processTransformValue(value) {
|
|
135
|
+
if (typeof value === 'string')
|
|
136
|
+
return processTransform(value);
|
|
137
|
+
if (Array.isArray(value))
|
|
138
|
+
return processTransform(value.filter(isRecord));
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function isStringOrNumber(value) {
|
|
142
|
+
return typeof value === 'string' || typeof value === 'number';
|
|
143
|
+
}
|
|
144
|
+
// Convert a prop to the shape Fabric's C++ expects. A third-party view contributes
|
|
145
|
+
// its own processors, auto-derived from its ViewConfig (validAttributes[*].process,
|
|
146
|
+
// e.g. processColor for a slider's track tints); those run first. Then the structured
|
|
147
|
+
// CSS-style processors (boxShadow/filter). Built-ins are never in the registry, so they
|
|
148
|
+
// fall through to the global color path, where any CSS-string color is run through the
|
|
149
|
+
// injected platform processor (Fabric's C++ color parser silently drops strings).
|
|
150
|
+
function processValue(component, key, value) {
|
|
151
|
+
const processor = registeredProcessor(component, key);
|
|
152
|
+
if (processor !== undefined)
|
|
153
|
+
return processor(value);
|
|
154
|
+
const styleProcessor = STYLE_PROCESSORS.get(key);
|
|
155
|
+
if (styleProcessor !== undefined)
|
|
156
|
+
return styleProcessor(value);
|
|
157
|
+
if (COLOR_PROPS.has(key) && isProcessableColor(value))
|
|
158
|
+
return processColor(value);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
// Translate the retained node's logical props into the flat payload Fabric's C++
|
|
162
|
+
// props expect: `style` keys are hoisted to the top level, event handlers and
|
|
163
|
+
// undefined values are dropped.
|
|
164
|
+
export function fabricProps(node) {
|
|
165
|
+
if (node.component === RAW_TEXT_COMPONENT) {
|
|
166
|
+
return { text: node.props.text };
|
|
167
|
+
}
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
170
|
+
if (key === 'style')
|
|
171
|
+
continue;
|
|
172
|
+
if (typeof value === 'function')
|
|
173
|
+
continue;
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
continue;
|
|
176
|
+
out[key] = processValue(node.component, key, value);
|
|
177
|
+
}
|
|
178
|
+
// Collapse style (object | array | nested arrays) into one flat payload before
|
|
179
|
+
// hoisting: `style={[base, override]}` is RN's idiom and Fabric wants it flat.
|
|
180
|
+
const style = flattenStyle(node.props.style);
|
|
181
|
+
for (const [key, value] of Object.entries(style)) {
|
|
182
|
+
if (value !== undefined)
|
|
183
|
+
out[key] = processValue(node.component, key, value);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
// swapLeftAndRightInRTL(flipStyles: boolean)
|
|
15
15
|
import { dlog } from '../debug.js';
|
|
16
16
|
import { getNativeModule } from '../native-modules.js';
|
|
17
|
+
import { isBoolean } from '../type-guards.js';
|
|
17
18
|
// The iOS native module name RN registers this under (the same name on both
|
|
18
|
-
// platforms). A module name is only provable on a real host
|
|
19
|
-
// answers to any name
|
|
19
|
+
// platforms). A module name is only provable on a real host (a headless fake
|
|
20
|
+
// answers to any name), so this name is still pending verification on device.
|
|
20
21
|
const I18N_MANAGER_MODULE = 'I18nManager';
|
|
21
22
|
// RN's fallback constants when no native module is linked (headless / not yet on
|
|
22
23
|
// device): not RTL, and the iOS default of swapping in RTL.
|
|
@@ -24,9 +25,6 @@ const DEFAULT_CONSTANTS = {
|
|
|
24
25
|
isRTL: false,
|
|
25
26
|
doLeftAndRightSwapInRTL: true,
|
|
26
27
|
};
|
|
27
|
-
function isBoolean(value) {
|
|
28
|
-
return typeof value === 'boolean';
|
|
29
|
-
}
|
|
30
28
|
function isOptionalString(value) {
|
|
31
29
|
return value === undefined || typeof value === 'string';
|
|
32
30
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type IImageSourceProp } from '../image-source-resolver';
|
|
2
|
+
export type IImageSize = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
|
|
7
|
+
type ISizeSuccess = (width: number, height: number) => void;
|
|
8
|
+
type ISizeFailure = (error: unknown) => void;
|
|
9
|
+
declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
|
|
10
|
+
declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
|
|
11
|
+
declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
|
|
12
|
+
declare function abortPrefetch(requestId: number): void;
|
|
13
|
+
declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
|
|
14
|
+
declare function resolveAssetSource(source: IImageSourceProp): unknown;
|
|
15
|
+
export type IImageStatics = {
|
|
16
|
+
getSize: typeof getSize;
|
|
17
|
+
getSizeWithHeaders: typeof getSizeWithHeaders;
|
|
18
|
+
prefetch: typeof prefetch;
|
|
19
|
+
abortPrefetch: typeof abortPrefetch;
|
|
20
|
+
queryCache: typeof queryCache;
|
|
21
|
+
resolveAssetSource: typeof resolveAssetSource;
|
|
22
|
+
};
|
|
23
|
+
export declare const imageStatics: IImageStatics;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Image static methods (RN's Image.getSize / prefetch / queryCache / etc).
|
|
2
|
+
//
|
|
3
|
+
// These mirror RN's iOS Image statics (Libraries/Image/Image.ios.js), which delegate to the
|
|
4
|
+
// `ImageLoader` native (Turbo)Module declared in NativeImageLoaderIOS.js. The Android spec
|
|
5
|
+
// (NativeImageLoaderAndroid.js) registers under the SAME module name ('ImageLoader'), so this
|
|
6
|
+
// stays a flat, non-platform-split module - only the Android prefetch call signature differs (a
|
|
7
|
+
// second `requestId` arg), branched on Platform.OS below, not on module name. NOTE the asymmetry
|
|
8
|
+
// in the iOS spec: `getSize` resolves a `[width, height]` ARRAY, while `getSizeWithHeaders`
|
|
9
|
+
// resolves a `{width, height}` OBJECT, both are guarded below before reading. The native result
|
|
10
|
+
// crosses the I/O boundary as `unknown`; we never cast it, we narrow its shape.
|
|
11
|
+
//
|
|
12
|
+
// This is a stateful, native-bridge-touching imperative module (module-level ImageLoader cache +
|
|
13
|
+
// prefetch requestId counter) with no view of its own - it belongs in @symbiote-native/engine
|
|
14
|
+
// alongside Alert/Share, not in a view/render-*.ts file (whose contract is zero state / zero
|
|
15
|
+
// native bridge).
|
|
16
|
+
import { dlog } from '../debug.js';
|
|
17
|
+
import { resolveImageSource } from '../image-source-resolver.js';
|
|
18
|
+
import { getNativeModule } from '../native-modules.js';
|
|
19
|
+
import { Platform } from '../platform';
|
|
20
|
+
import { isNumber } from '../type-guards.js';
|
|
21
|
+
// The iOS native module name RN registers this under (NativeImageLoaderIOS.js resolves
|
|
22
|
+
// `TurboModuleRegistry.getEnforcing<Spec>('ImageLoader')`). A module name like this is only
|
|
23
|
+
// provable on a real host - a headless fake answers to any name - so this iOS name is
|
|
24
|
+
// device-verify-pending.
|
|
25
|
+
const IMAGE_LOADER_MODULE = 'ImageLoader';
|
|
26
|
+
let imageLoaderModule;
|
|
27
|
+
function getImageLoader() {
|
|
28
|
+
if (imageLoaderModule === undefined) {
|
|
29
|
+
imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
|
|
30
|
+
dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
|
|
31
|
+
}
|
|
32
|
+
return imageLoaderModule;
|
|
33
|
+
}
|
|
34
|
+
// Narrow native's getSize result. The spec resolves a `[width, height]` array, but tolerate a
|
|
35
|
+
// `{width, height}` object too (getSizeWithHeaders uses that shape).
|
|
36
|
+
function toImageSize(result) {
|
|
37
|
+
if (Array.isArray(result) && isNumber(result[0]) && isNumber(result[1])) {
|
|
38
|
+
return { width: result[0], height: result[1] };
|
|
39
|
+
}
|
|
40
|
+
if (typeof result === 'object' && result !== null) {
|
|
41
|
+
const width = Reflect.get(result, 'width');
|
|
42
|
+
const height = Reflect.get(result, 'height');
|
|
43
|
+
if (isNumber(width) && isNumber(height))
|
|
44
|
+
return { width, height };
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Image: unexpected size result from native: ${JSON.stringify(result)}`);
|
|
47
|
+
}
|
|
48
|
+
function requireLoader(method) {
|
|
49
|
+
const loader = getImageLoader();
|
|
50
|
+
if (loader === null) {
|
|
51
|
+
throw new Error(`Image.${method}: ImageLoader native module is not available ` +
|
|
52
|
+
'(running headless or not linked on this host).');
|
|
53
|
+
}
|
|
54
|
+
return loader;
|
|
55
|
+
}
|
|
56
|
+
// Resolve image dimensions, optionally via success/failure callbacks. Always returns the Promise
|
|
57
|
+
// too (RN returns void when a callback is passed, but a promise-and-callback shape is friendlier
|
|
58
|
+
// and a strict superset).
|
|
59
|
+
function getSize(uri, success, failure) {
|
|
60
|
+
const promise = Promise.resolve()
|
|
61
|
+
.then(() => requireLoader('getSize').getSize(uri))
|
|
62
|
+
.then(toImageSize);
|
|
63
|
+
if (typeof success === 'function') {
|
|
64
|
+
promise
|
|
65
|
+
.then(size => success(size.width, size.height))
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
if (typeof failure === 'function')
|
|
68
|
+
failure(error);
|
|
69
|
+
else
|
|
70
|
+
dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return promise;
|
|
74
|
+
}
|
|
75
|
+
function getSizeWithHeaders(uri, headers, success, failure) {
|
|
76
|
+
const promise = Promise.resolve()
|
|
77
|
+
.then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
|
|
78
|
+
.then(toImageSize);
|
|
79
|
+
if (typeof success === 'function') {
|
|
80
|
+
promise
|
|
81
|
+
.then(size => success(size.width, size.height))
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
if (typeof failure === 'function')
|
|
84
|
+
failure(error);
|
|
85
|
+
else
|
|
86
|
+
dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return promise;
|
|
90
|
+
}
|
|
91
|
+
// Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can cancel it);
|
|
92
|
+
// RN's Image.android.js generates the same way. iOS ignores the arg.
|
|
93
|
+
let prefetchRequestId = 0;
|
|
94
|
+
// Download a remote image into the disk cache. Resolves to whether it succeeded. `callback`
|
|
95
|
+
// receives the requestId (RN's Image.android.js shape) so the caller can later pass it to
|
|
96
|
+
// abortPrefetch.
|
|
97
|
+
async function prefetch(uri, callback) {
|
|
98
|
+
prefetchRequestId += 1;
|
|
99
|
+
const requestId = prefetchRequestId;
|
|
100
|
+
if (typeof callback === 'function')
|
|
101
|
+
callback(requestId);
|
|
102
|
+
const loader = requireLoader('prefetch');
|
|
103
|
+
return (Promise.resolve()
|
|
104
|
+
// Android's prefetchImage keys an abortable request on requestId; iOS takes ONLY the uri and
|
|
105
|
+
// throws on an extra arg (bridgeless TurboModule arg-count check). Match RN's per-platform call.
|
|
106
|
+
.then(() => Platform.OS === 'android'
|
|
107
|
+
? loader.prefetchImage(uri, requestId)
|
|
108
|
+
: loader.prefetchImage(uri))
|
|
109
|
+
.then(result => result === true)
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
dlog(`Image.prefetch failed for ${uri}: ${String(error)}`);
|
|
112
|
+
throw error;
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
// Cancel an in-flight prefetch by the requestId prefetch handed back. Android only (mirrors
|
|
116
|
+
// Image.android.js -> NativeImageLoaderAndroid.abortRequest); a missing abortRequest (iOS,
|
|
117
|
+
// headless) is a no-op rather than a throw.
|
|
118
|
+
function abortPrefetch(requestId) {
|
|
119
|
+
const loader = getImageLoader();
|
|
120
|
+
if (loader === null || typeof loader.abortRequest !== 'function') {
|
|
121
|
+
dlog(`Image.abortPrefetch(${requestId}): no abortRequest on this host, ignoring`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
loader.abortRequest(requestId);
|
|
125
|
+
}
|
|
126
|
+
// Narrow native's queryCache result: an object mapping each known uri to its cache status.
|
|
127
|
+
// Unknown statuses are dropped rather than trusted blindly.
|
|
128
|
+
const CACHE_STATUS = {
|
|
129
|
+
memory: 'memory',
|
|
130
|
+
disk: 'disk',
|
|
131
|
+
'disk/memory': 'disk/memory',
|
|
132
|
+
};
|
|
133
|
+
function toCacheRecord(result) {
|
|
134
|
+
const record = {};
|
|
135
|
+
if (typeof result !== 'object' || result === null)
|
|
136
|
+
return record;
|
|
137
|
+
for (const key of Object.keys(result)) {
|
|
138
|
+
const value = Reflect.get(result, key);
|
|
139
|
+
if (typeof value === 'string' && Object.hasOwn(CACHE_STATUS, value)) {
|
|
140
|
+
record[key] = CACHE_STATUS[value];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return record;
|
|
144
|
+
}
|
|
145
|
+
async function queryCache(uris) {
|
|
146
|
+
return Promise.resolve()
|
|
147
|
+
.then(() => {
|
|
148
|
+
const loader = requireLoader('queryCache');
|
|
149
|
+
// The native queryCache never rejects (RCTImageLoader resolves getImageCacheStatus), so a
|
|
150
|
+
// rejection here is a JS/native boundary fault: log whether the method is even callable and
|
|
151
|
+
// the arg shape, to tell "not a function" (interop gap) from a marshalling reject.
|
|
152
|
+
dlog(`Image.queryCache: typeof loader.queryCache=${typeof loader.queryCache} uris=${uris.length}`);
|
|
153
|
+
return loader.queryCache(uris);
|
|
154
|
+
})
|
|
155
|
+
.then(toCacheRecord)
|
|
156
|
+
.catch((error) => {
|
|
157
|
+
dlog(`Image.queryCache failed: ${String(error)}`);
|
|
158
|
+
throw error;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// PURE JS: run the currently-installed source resolver (the same machinery the Image component
|
|
162
|
+
// uses via resolveImageSource). RN's resolveAssetSource turns a require() asset id into
|
|
163
|
+
// {uri, scale, ...}; the app injects the real one with setImageSourceResolver, and this exposes
|
|
164
|
+
// its output to callers directly.
|
|
165
|
+
function resolveAssetSource(source) {
|
|
166
|
+
return resolveImageSource(source);
|
|
167
|
+
}
|
|
168
|
+
export const imageStatics = {
|
|
169
|
+
getSize,
|
|
170
|
+
getSizeWithHeaders,
|
|
171
|
+
prefetch,
|
|
172
|
+
abortPrefetch,
|
|
173
|
+
queryCache,
|
|
174
|
+
resolveAssetSource,
|
|
175
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type IImageSourceProp } from './image-source-resolver';
|
|
2
|
+
export type IImageSize = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
|
|
7
|
+
type ISizeSuccess = (width: number, height: number) => void;
|
|
8
|
+
type ISizeFailure = (error: unknown) => void;
|
|
9
|
+
declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
|
|
10
|
+
declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
|
|
11
|
+
declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
|
|
12
|
+
declare function abortPrefetch(requestId: number): void;
|
|
13
|
+
declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
|
|
14
|
+
declare function resolveAssetSource(source: IImageSourceProp): unknown;
|
|
15
|
+
export type IImageStatics = {
|
|
16
|
+
getSize: typeof getSize;
|
|
17
|
+
getSizeWithHeaders: typeof getSizeWithHeaders;
|
|
18
|
+
prefetch: typeof prefetch;
|
|
19
|
+
abortPrefetch: typeof abortPrefetch;
|
|
20
|
+
queryCache: typeof queryCache;
|
|
21
|
+
resolveAssetSource: typeof resolveAssetSource;
|
|
22
|
+
};
|
|
23
|
+
export declare const imageStatics: IImageStatics;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Image static methods (RN's Image.getSize / prefetch / queryCache / etc).
|
|
2
|
+
//
|
|
3
|
+
// These mirror RN's iOS Image statics (Libraries/Image/Image.ios.js), which delegate to the
|
|
4
|
+
// `ImageLoader` native (Turbo)Module declared in NativeImageLoaderIOS.js. The Android spec
|
|
5
|
+
// (NativeImageLoaderAndroid.js) registers under the SAME module name ('ImageLoader'), so this
|
|
6
|
+
// stays a flat, non-platform-split module - only the Android prefetch call signature differs (a
|
|
7
|
+
// second `requestId` arg), branched on Platform.OS below, not on module name. NOTE the asymmetry
|
|
8
|
+
// in the iOS spec: `getSize` resolves a `[width, height]` ARRAY, while `getSizeWithHeaders`
|
|
9
|
+
// resolves a `{width, height}` OBJECT, both are guarded below before reading. The native result
|
|
10
|
+
// crosses the I/O boundary as `unknown`; we never cast it, we narrow its shape.
|
|
11
|
+
//
|
|
12
|
+
// This is a stateful, native-bridge-touching imperative module (module-level ImageLoader cache +
|
|
13
|
+
// prefetch requestId counter) with no view of its own - it belongs in @symbiote-native/engine
|
|
14
|
+
// alongside Alert/Share, not in a view/render-*.ts file (whose contract is zero state / zero
|
|
15
|
+
// native bridge).
|
|
16
|
+
import { dlog } from './debug.js';
|
|
17
|
+
import { resolveImageSource } from './image-source-resolver.js';
|
|
18
|
+
import { getNativeModule } from './native-modules.js';
|
|
19
|
+
import { Platform } from './platform';
|
|
20
|
+
import { isNumber } from './type-guards.js';
|
|
21
|
+
// The iOS native module name RN registers this under (NativeImageLoaderIOS.js resolves
|
|
22
|
+
// `TurboModuleRegistry.getEnforcing<Spec>('ImageLoader')`). A module name like this is only
|
|
23
|
+
// provable on a real host - a headless fake answers to any name - so this iOS name is
|
|
24
|
+
// device-verify-pending.
|
|
25
|
+
const IMAGE_LOADER_MODULE = 'ImageLoader';
|
|
26
|
+
let imageLoaderModule;
|
|
27
|
+
function getImageLoader() {
|
|
28
|
+
if (imageLoaderModule === undefined) {
|
|
29
|
+
imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
|
|
30
|
+
dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
|
|
31
|
+
}
|
|
32
|
+
return imageLoaderModule;
|
|
33
|
+
}
|
|
34
|
+
// Narrow native's getSize result. The spec resolves a `[width, height]` array, but tolerate a
|
|
35
|
+
// `{width, height}` object too (getSizeWithHeaders uses that shape).
|
|
36
|
+
function toImageSize(result) {
|
|
37
|
+
if (Array.isArray(result) && isNumber(result[0]) && isNumber(result[1])) {
|
|
38
|
+
return { width: result[0], height: result[1] };
|
|
39
|
+
}
|
|
40
|
+
if (typeof result === 'object' && result !== null) {
|
|
41
|
+
const width = Reflect.get(result, 'width');
|
|
42
|
+
const height = Reflect.get(result, 'height');
|
|
43
|
+
if (isNumber(width) && isNumber(height))
|
|
44
|
+
return { width, height };
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Image: unexpected size result from native: ${JSON.stringify(result)}`);
|
|
47
|
+
}
|
|
48
|
+
function requireLoader(method) {
|
|
49
|
+
const loader = getImageLoader();
|
|
50
|
+
if (loader === null) {
|
|
51
|
+
throw new Error(`Image.${method}: ImageLoader native module is not available ` +
|
|
52
|
+
'(running headless or not linked on this host).');
|
|
53
|
+
}
|
|
54
|
+
return loader;
|
|
55
|
+
}
|
|
56
|
+
// Resolve image dimensions, optionally via success/failure callbacks. Always returns the Promise
|
|
57
|
+
// too (RN returns void when a callback is passed, but a promise-and-callback shape is friendlier
|
|
58
|
+
// and a strict superset).
|
|
59
|
+
function getSize(uri, success, failure) {
|
|
60
|
+
const promise = Promise.resolve()
|
|
61
|
+
.then(() => requireLoader('getSize').getSize(uri))
|
|
62
|
+
.then(toImageSize);
|
|
63
|
+
if (typeof success === 'function') {
|
|
64
|
+
promise
|
|
65
|
+
.then(size => success(size.width, size.height))
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
if (typeof failure === 'function')
|
|
68
|
+
failure(error);
|
|
69
|
+
else
|
|
70
|
+
dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return promise;
|
|
74
|
+
}
|
|
75
|
+
function getSizeWithHeaders(uri, headers, success, failure) {
|
|
76
|
+
const promise = Promise.resolve()
|
|
77
|
+
.then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
|
|
78
|
+
.then(toImageSize);
|
|
79
|
+
if (typeof success === 'function') {
|
|
80
|
+
promise
|
|
81
|
+
.then(size => success(size.width, size.height))
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
if (typeof failure === 'function')
|
|
84
|
+
failure(error);
|
|
85
|
+
else
|
|
86
|
+
dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return promise;
|
|
90
|
+
}
|
|
91
|
+
// Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can cancel it);
|
|
92
|
+
// RN's Image.android.js generates the same way. iOS ignores the arg.
|
|
93
|
+
let prefetchRequestId = 0;
|
|
94
|
+
// Download a remote image into the disk cache. Resolves to whether it succeeded. `callback`
|
|
95
|
+
// receives the requestId (RN's Image.android.js shape) so the caller can later pass it to
|
|
96
|
+
// abortPrefetch.
|
|
97
|
+
async function prefetch(uri, callback) {
|
|
98
|
+
prefetchRequestId += 1;
|
|
99
|
+
const requestId = prefetchRequestId;
|
|
100
|
+
if (typeof callback === 'function')
|
|
101
|
+
callback(requestId);
|
|
102
|
+
const loader = requireLoader('prefetch');
|
|
103
|
+
return (Promise.resolve()
|
|
104
|
+
// Android's prefetchImage keys an abortable request on requestId; iOS takes ONLY the uri and
|
|
105
|
+
// throws on an extra arg (bridgeless TurboModule arg-count check). Match RN's per-platform call.
|
|
106
|
+
.then(() => Platform.OS === 'android'
|
|
107
|
+
? loader.prefetchImage(uri, requestId)
|
|
108
|
+
: loader.prefetchImage(uri))
|
|
109
|
+
.then(result => result === true)
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
dlog(`Image.prefetch failed for ${uri}: ${String(error)}`);
|
|
112
|
+
throw error;
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
// Cancel an in-flight prefetch by the requestId prefetch handed back. Android only (mirrors
|
|
116
|
+
// Image.android.js -> NativeImageLoaderAndroid.abortRequest); a missing abortRequest (iOS,
|
|
117
|
+
// headless) is a no-op rather than a throw.
|
|
118
|
+
function abortPrefetch(requestId) {
|
|
119
|
+
const loader = getImageLoader();
|
|
120
|
+
if (loader === null || typeof loader.abortRequest !== 'function') {
|
|
121
|
+
dlog(`Image.abortPrefetch(${requestId}): no abortRequest on this host, ignoring`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
loader.abortRequest(requestId);
|
|
125
|
+
}
|
|
126
|
+
// Narrow native's queryCache result: an object mapping each known uri to its cache status.
|
|
127
|
+
// Unknown statuses are dropped rather than trusted blindly.
|
|
128
|
+
const CACHE_STATUS = {
|
|
129
|
+
memory: 'memory',
|
|
130
|
+
disk: 'disk',
|
|
131
|
+
'disk/memory': 'disk/memory',
|
|
132
|
+
};
|
|
133
|
+
function toCacheRecord(result) {
|
|
134
|
+
const record = {};
|
|
135
|
+
if (typeof result !== 'object' || result === null)
|
|
136
|
+
return record;
|
|
137
|
+
for (const key of Object.keys(result)) {
|
|
138
|
+
const value = Reflect.get(result, key);
|
|
139
|
+
if (typeof value === 'string' && Object.hasOwn(CACHE_STATUS, value)) {
|
|
140
|
+
record[key] = CACHE_STATUS[value];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return record;
|
|
144
|
+
}
|
|
145
|
+
async function queryCache(uris) {
|
|
146
|
+
return Promise.resolve()
|
|
147
|
+
.then(() => {
|
|
148
|
+
const loader = requireLoader('queryCache');
|
|
149
|
+
// The native queryCache never rejects (RCTImageLoader resolves getImageCacheStatus), so a
|
|
150
|
+
// rejection here is a JS/native boundary fault: log whether the method is even callable and
|
|
151
|
+
// the arg shape, to tell "not a function" (interop gap) from a marshalling reject.
|
|
152
|
+
dlog(`Image.queryCache: typeof loader.queryCache=${typeof loader.queryCache} uris=${uris.length}`);
|
|
153
|
+
return loader.queryCache(uris);
|
|
154
|
+
})
|
|
155
|
+
.then(toCacheRecord)
|
|
156
|
+
.catch((error) => {
|
|
157
|
+
dlog(`Image.queryCache failed: ${String(error)}`);
|
|
158
|
+
throw error;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// PURE JS: run the currently-installed source resolver (the same machinery the Image component
|
|
162
|
+
// uses via resolveImageSource). RN's resolveAssetSource turns a require() asset id into
|
|
163
|
+
// {uri, scale, ...}; the app injects the real one with setImageSourceResolver, and this exposes
|
|
164
|
+
// its output to callers directly.
|
|
165
|
+
function resolveAssetSource(source) {
|
|
166
|
+
return resolveImageSource(source);
|
|
167
|
+
}
|
|
168
|
+
export const imageStatics = {
|
|
169
|
+
getSize,
|
|
170
|
+
getSizeWithHeaders,
|
|
171
|
+
prefetch,
|
|
172
|
+
abortPrefetch,
|
|
173
|
+
queryCache,
|
|
174
|
+
resolveAssetSource,
|
|
175
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function setImageSourceResolver(resolve: (source: unknown) => unknown): void;
|
|
2
|
+
export declare function resolveImageSource(source: unknown): unknown;
|
|
3
|
+
export type IImageSource = {
|
|
4
|
+
uri?: string;
|
|
5
|
+
scale?: number;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
};
|
|
9
|
+
export type IImageSourceProp = IImageSource | IImageSource[] | number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Image source resolution seam: require('./x.png') asset ids and {uri} sources are resolved by
|
|
2
|
+
// RN's own resolveAssetSource before reaching the shared render fn. The actual resolution is
|
|
3
|
+
// RN-platform-specific, so it is injected here rather than imported, mirroring platform-color.ts's
|
|
4
|
+
// processColor seam - this keeps @symbiote-native/components free of a react-native dependency (and
|
|
5
|
+
// the headless harness working). BOTH the pure renderImage view (@symbiote-native/components) and
|
|
6
|
+
// this package's own image-loader statics (resolveAssetSource) call resolveImageSource; neither
|
|
7
|
+
// reaches into the mutable resolver directly.
|
|
8
|
+
let sourceResolver = source => source;
|
|
9
|
+
export function setImageSourceResolver(resolve) {
|
|
10
|
+
sourceResolver = resolve;
|
|
11
|
+
}
|
|
12
|
+
// Public mirror of RN's Image.resolveAssetSource: run a source through the injected resolver.
|
|
13
|
+
// Headless (no resolver wired) it is the identity, so smokes see the input unchanged.
|
|
14
|
+
export function resolveImageSource(source) {
|
|
15
|
+
return sourceResolver(source);
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function setImageSourceResolver(resolve: (source: unknown) => unknown): void;
|
|
2
|
+
export declare function resolveImageSource(source: unknown): unknown;
|
|
3
|
+
export type IImageSource = {
|
|
4
|
+
uri?: string;
|
|
5
|
+
scale?: number;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
};
|
|
9
|
+
export type IImageSourceProp = IImageSource | IImageSource[] | number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Image source resolution seam: require('./x.png') asset ids and {uri} sources are resolved by
|
|
2
|
+
// RN's own resolveAssetSource before reaching the shared render fn. The actual resolution is
|
|
3
|
+
// RN-platform-specific, so it is injected here rather than imported, mirroring platform-color.ts's
|
|
4
|
+
// processColor seam - this keeps @symbiote-native/components free of a react-native dependency (and
|
|
5
|
+
// the headless harness working). BOTH the pure renderImage view (@symbiote-native/components) and
|
|
6
|
+
// this package's own image-loader statics (resolveAssetSource) call resolveImageSource; neither
|
|
7
|
+
// reaches into the mutable resolver directly.
|
|
8
|
+
let sourceResolver = source => source;
|
|
9
|
+
export function setImageSourceResolver(resolve) {
|
|
10
|
+
sourceResolver = resolve;
|
|
11
|
+
}
|
|
12
|
+
// Public mirror of RN's Image.resolveAssetSource: run a source through the injected resolver.
|
|
13
|
+
// Headless (no resolver wired) it is the identity, so smokes see the input unchanged.
|
|
14
|
+
export function resolveImageSource(source) {
|
|
15
|
+
return sourceResolver(source);
|
|
16
|
+
}
|