@symbiote-native/engine 0.1.6 → 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.
Files changed (67) hide show
  1. package/build/accessibility-info/index.android.js +1 -1
  2. package/build/accessibility-info/index.ios.js +1 -1
  3. package/build/action-sheet-ios/index.js +1 -1
  4. package/build/alert/index.android.js +1 -1
  5. package/build/alert/index.ios.js +11 -1
  6. package/build/animated/color.d.ts +2 -8
  7. package/build/animated/color.js +4 -45
  8. package/build/animated/easing.js +0 -8
  9. package/build/animated/graph.d.ts +13 -5
  10. package/build/animated/graph.js +85 -11
  11. package/build/animated/index.d.ts +2 -2
  12. package/build/animated/index.js +2 -2
  13. package/build/animated/interpolation.js +1 -1
  14. package/build/animated/leaf-lifecycle.d.ts +7 -0
  15. package/build/animated/leaf-lifecycle.js +185 -0
  16. package/build/animated/native/native-animated.js +2 -1
  17. package/build/animated/rgba.d.ts +8 -0
  18. package/build/animated/rgba.js +55 -0
  19. package/build/animated/value.js +12 -7
  20. package/build/app-registry/index.js +1 -1
  21. package/build/app-state/index.js +1 -1
  22. package/build/appearance/index.js +1 -1
  23. package/build/back-handler/index.js +1 -1
  24. package/build/commit.js +1 -5
  25. package/build/debug.d.ts +1 -1
  26. package/build/debug.js +10 -3
  27. package/build/dimensions/index.js +1 -1
  28. package/build/events/index.js +0 -2
  29. package/build/i18n-manager/index.js +1 -1
  30. package/build/image-loader.js +1 -1
  31. package/build/index.d.ts +4 -2
  32. package/build/index.js +3 -2
  33. package/build/keyboard/index.js +1 -1
  34. package/build/layout-animation/index.js +1 -2
  35. package/build/linking/shared.js +1 -1
  36. package/build/permissions-android/index.js +1 -1
  37. package/build/platform/shared.js +1 -1
  38. package/build/process-font-variant.js +0 -1
  39. package/build/process-transform/index.js +13 -19
  40. package/build/report-error.d.ts +5 -0
  41. package/build/report-error.js +60 -0
  42. package/build/settings/index.js +1 -1
  43. package/build/share/index.android.js +1 -1
  44. package/build/share/index.ios.js +1 -1
  45. package/build/status-bar/index.android.js +6 -7
  46. package/build/status-bar/index.ios.js +1 -1
  47. package/build/style-registry/index.js +95 -16
  48. package/build/style-sheet/index.js +5 -3
  49. package/build/surface.js +2 -1
  50. package/build/toast-android/index.js +1 -1
  51. package/build/vibration/index.android.js +2 -3
  52. package/build/vibration/shared.js +1 -1
  53. package/package.json +3 -2
  54. package/build/animated/interpolation-node.d.ts +0 -15
  55. package/build/animated/interpolation-node.js +0 -58
  56. package/build/image-loader/index.d.ts +0 -24
  57. package/build/image-loader/index.js +0 -175
  58. package/build/image-source-resolver/index.d.ts +0 -9
  59. package/build/image-source-resolver/index.js +0 -16
  60. package/build/native-modules.d.ts +0 -18
  61. package/build/native-modules.js +0 -120
  62. package/build/style-registry/scope/index.d.ts +0 -3
  63. package/build/style-registry/scope/index.js +0 -41
  64. package/build/touch-history/index.d.ts +0 -23
  65. package/build/touch-history/index.js +0 -150
  66. package/build/type-guards/index.d.ts +0 -4
  67. package/build/type-guards/index.js +0 -15
@@ -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 joined by
15
- // '.' (e.g. "btn primary" -> "btn.primary" / "primary.btn") before falling back to a
16
- // per-class merge, mirroring CSS compound-selector registration (`.btn.primary { }`).
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 exactMatch = globalStyles.get(trimmed) ?? globalStyles.get(kebabToCamel(trimmed));
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 parts = trimmed.split(/\s+/).filter(Boolean);
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 parts.reduce((acc, cls) => {
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(toCompoundKey(current));
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 compound of generateCompoundPermutations(parts)) {
117
- const style = globalStyles.get(compound);
118
- if (style)
119
- return style;
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
- return globalStyles.get(trimmed) ?? globalStyles.get(kebabToCamel(trimmed)) ?? {};
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
  }
@@ -5,7 +5,7 @@
5
5
  // constants. The typed ViewStyle/TextStyle live next door in styles.ts, so `create`
6
6
  // is typed against them like RN's, not left generic over a plain Record.
7
7
  import { dlog } from '../debug.js';
8
- import { getNativeModule } from '../native-modules.js';
8
+ import { getNativeModule } from '../native-modules/index.js';
9
9
  import { flattenStyle } from '../style/index.js';
10
10
  // RN's hairline factor: a "1 physical pixel" line is ~0.4 logical px, rounded to the
11
11
  // nearest device pixel. Named so the 0.4 isn't a bare magic number (RN source uses
@@ -49,8 +49,10 @@ const absoluteFill = Object.freeze({
49
49
  top: 0,
50
50
  bottom: 0,
51
51
  });
52
- // RN's composeStyles: falsy left → right, falsy right → left, else the pair [a, b]
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 new SymbioteSurface(rootTag);
67
+ return surface;
67
68
  }
@@ -14,7 +14,7 @@
14
14
  // showWithGravity(message: string, duration: number, gravity: number)
15
15
  // showWithGravityAndOffset(message, duration, gravity, xOffset, yOffset)
16
16
  import { dlog } from '../debug.js';
17
- import { getNativeModule } from '../native-modules.js';
17
+ import { getNativeModule } from '../native-modules/index.js';
18
18
  // The native module name RN registers this under. NOTE: this is the name the spec
19
19
  // resolves via `TurboModuleRegistry.getEnforcing<Spec>('ToastAndroid')`, NOT the
20
20
  // spec filename `INativeToastAndroid`. A module name is only provable on a real
@@ -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)`: RN encodes "do not repeat" as -1
3
- // and "repeat from start" as 0. Everything else is the shared core. Metro picks this file
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
@@ -16,7 +16,7 @@
16
16
  // the base vibration.ts re-exports the iOS build for web/headless. There is no runtime
17
17
  // `Platform.OS` read; the filename is the selector. No Fabric view, pure JS->native.
18
18
  import { dlog } from '../debug.js';
19
- import { getNativeModule } from '../native-modules.js';
19
+ import { getNativeModule } from '../native-modules/index.js';
20
20
  const VIBRATION_MODULE = 'Vibration';
21
21
  // RN's _default_vibration_length: the single-buzz duration when no pattern is given,
22
22
  // and the per-segment buzz length the iOS scheduler emits between waits.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@symbiote-native/engine",
3
- "version": "0.1.6",
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
+ "license": "MIT",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
@@ -32,7 +33,7 @@
32
33
  "react-native": ">=0.86"
33
34
  },
34
35
  "devDependencies": {
35
- "@symbiote-native/test-utils": "0.1.5"
36
+ "@symbiote-native/test-utils": "0.1.6"
36
37
  },
37
38
  "scripts": {
38
39
  "typecheck": "tsc --build",
@@ -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,24 +0,0 @@
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 {};
@@ -1,175 +0,0 @@
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
- };
@@ -1,9 +0,0 @@
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;
@@ -1,16 +0,0 @@
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
- }
@@ -1,18 +0,0 @@
1
- import { NativeEventEmitter } from './native-events';
2
- declare global {
3
- var __turboModuleProxy: (<T>(name: string) => T | null) | undefined;
4
- var nativeModuleProxy: Record<string, unknown> | undefined;
5
- }
6
- export declare function getNativeModule<T>(name: string): T | null;
7
- export declare function getEnforcingNativeModule<T>(name: string): T;
8
- export interface IDeviceEventModuleConfig<TModule> {
9
- moduleName: string;
10
- moduleLogPrefix: string;
11
- bindModuleToEmitter?: boolean;
12
- onEmitterCreated?: (emitter: NativeEventEmitter, module: TModule | null) => void;
13
- }
14
- export interface IDeviceEventModule<TModule> {
15
- getModule(): TModule | null;
16
- getEmitter(): NativeEventEmitter;
17
- }
18
- export declare function createDeviceEventModule<TModule>(config: IDeviceEventModuleConfig<TModule>): IDeviceEventModule<TModule>;