@buoy-gg/images 7.0.3

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 (195) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +58 -0
  3. package/lib/commonjs/capture/actions.js +227 -0
  4. package/lib/commonjs/capture/actions.js.map +1 -0
  5. package/lib/commonjs/capture/install.js +41 -0
  6. package/lib/commonjs/capture/install.js.map +1 -0
  7. package/lib/commonjs/capture/installExpoImage.js +99 -0
  8. package/lib/commonjs/capture/installExpoImage.js.map +1 -0
  9. package/lib/commonjs/capture/installRnImage.js +128 -0
  10. package/lib/commonjs/capture/installRnImage.js.map +1 -0
  11. package/lib/commonjs/capture/overrides.js +286 -0
  12. package/lib/commonjs/capture/overrides.js.map +1 -0
  13. package/lib/commonjs/capture/savings.js +193 -0
  14. package/lib/commonjs/capture/savings.js.map +1 -0
  15. package/lib/commonjs/capture/shared.js +367 -0
  16. package/lib/commonjs/capture/shared.js.map +1 -0
  17. package/lib/commonjs/components/ImageDetail.js +444 -0
  18. package/lib/commonjs/components/ImageDetail.js.map +1 -0
  19. package/lib/commonjs/components/ImageDetailFooter.js +232 -0
  20. package/lib/commonjs/components/ImageDetailFooter.js.map +1 -0
  21. package/lib/commonjs/components/ImageRow.js +164 -0
  22. package/lib/commonjs/components/ImageRow.js.map +1 -0
  23. package/lib/commonjs/components/ImagesHeader.js +63 -0
  24. package/lib/commonjs/components/ImagesHeader.js.map +1 -0
  25. package/lib/commonjs/components/ImagesIcon.js +59 -0
  26. package/lib/commonjs/components/ImagesIcon.js.map +1 -0
  27. package/lib/commonjs/components/ImagesListFooter.js +194 -0
  28. package/lib/commonjs/components/ImagesListFooter.js.map +1 -0
  29. package/lib/commonjs/components/ImagesModal.js +173 -0
  30. package/lib/commonjs/components/ImagesModal.js.map +1 -0
  31. package/lib/commonjs/components/ImagesRoot.js +24 -0
  32. package/lib/commonjs/components/ImagesRoot.js.map +1 -0
  33. package/lib/commonjs/components/InsightsBar.js +59 -0
  34. package/lib/commonjs/components/InsightsBar.js.map +1 -0
  35. package/lib/commonjs/export.js +81 -0
  36. package/lib/commonjs/export.js.map +1 -0
  37. package/lib/commonjs/format.js +117 -0
  38. package/lib/commonjs/format.js.map +1 -0
  39. package/lib/commonjs/hooks/useImageRecords.js +20 -0
  40. package/lib/commonjs/hooks/useImageRecords.js.map +1 -0
  41. package/lib/commonjs/index.js +236 -0
  42. package/lib/commonjs/index.js.map +1 -0
  43. package/lib/commonjs/package.json +1 -0
  44. package/lib/commonjs/preset.js +45 -0
  45. package/lib/commonjs/preset.js.map +1 -0
  46. package/lib/commonjs/register.js +19 -0
  47. package/lib/commonjs/register.js.map +1 -0
  48. package/lib/commonjs/state.js +54 -0
  49. package/lib/commonjs/state.js.map +1 -0
  50. package/lib/commonjs/store/imageEventStore.js +160 -0
  51. package/lib/commonjs/store/imageEventStore.js.map +1 -0
  52. package/lib/commonjs/store/insights.js +76 -0
  53. package/lib/commonjs/store/insights.js.map +1 -0
  54. package/lib/commonjs/sync/imagesSyncAdapter.js +315 -0
  55. package/lib/commonjs/sync/imagesSyncAdapter.js.map +1 -0
  56. package/lib/commonjs/types.js +2 -0
  57. package/lib/commonjs/types.js.map +1 -0
  58. package/lib/module/capture/actions.js +214 -0
  59. package/lib/module/capture/actions.js.map +1 -0
  60. package/lib/module/capture/install.js +36 -0
  61. package/lib/module/capture/install.js.map +1 -0
  62. package/lib/module/capture/installExpoImage.js +94 -0
  63. package/lib/module/capture/installExpoImage.js.map +1 -0
  64. package/lib/module/capture/installRnImage.js +122 -0
  65. package/lib/module/capture/installRnImage.js.map +1 -0
  66. package/lib/module/capture/overrides.js +269 -0
  67. package/lib/module/capture/overrides.js.map +1 -0
  68. package/lib/module/capture/savings.js +189 -0
  69. package/lib/module/capture/savings.js.map +1 -0
  70. package/lib/module/capture/shared.js +359 -0
  71. package/lib/module/capture/shared.js.map +1 -0
  72. package/lib/module/components/ImageDetail.js +440 -0
  73. package/lib/module/components/ImageDetail.js.map +1 -0
  74. package/lib/module/components/ImageDetailFooter.js +228 -0
  75. package/lib/module/components/ImageDetailFooter.js.map +1 -0
  76. package/lib/module/components/ImageRow.js +160 -0
  77. package/lib/module/components/ImageRow.js.map +1 -0
  78. package/lib/module/components/ImagesHeader.js +59 -0
  79. package/lib/module/components/ImagesHeader.js.map +1 -0
  80. package/lib/module/components/ImagesIcon.js +55 -0
  81. package/lib/module/components/ImagesIcon.js.map +1 -0
  82. package/lib/module/components/ImagesListFooter.js +190 -0
  83. package/lib/module/components/ImagesListFooter.js.map +1 -0
  84. package/lib/module/components/ImagesModal.js +169 -0
  85. package/lib/module/components/ImagesModal.js.map +1 -0
  86. package/lib/module/components/ImagesRoot.js +20 -0
  87. package/lib/module/components/ImagesRoot.js.map +1 -0
  88. package/lib/module/components/InsightsBar.js +55 -0
  89. package/lib/module/components/InsightsBar.js.map +1 -0
  90. package/lib/module/export.js +76 -0
  91. package/lib/module/export.js.map +1 -0
  92. package/lib/module/format.js +108 -0
  93. package/lib/module/format.js.map +1 -0
  94. package/lib/module/hooks/useImageRecords.js +16 -0
  95. package/lib/module/hooks/useImageRecords.js.map +1 -0
  96. package/lib/module/index.js +73 -0
  97. package/lib/module/index.js.map +1 -0
  98. package/lib/module/preset.js +40 -0
  99. package/lib/module/preset.js.map +1 -0
  100. package/lib/module/register.js +19 -0
  101. package/lib/module/register.js.map +1 -0
  102. package/lib/module/state.js +50 -0
  103. package/lib/module/state.js.map +1 -0
  104. package/lib/module/store/imageEventStore.js +146 -0
  105. package/lib/module/store/imageEventStore.js.map +1 -0
  106. package/lib/module/store/insights.js +71 -0
  107. package/lib/module/store/insights.js.map +1 -0
  108. package/lib/module/sync/imagesSyncAdapter.js +311 -0
  109. package/lib/module/sync/imagesSyncAdapter.js.map +1 -0
  110. package/lib/module/types.js +2 -0
  111. package/lib/module/types.js.map +1 -0
  112. package/lib/typescript/capture/actions.d.ts +49 -0
  113. package/lib/typescript/capture/actions.d.ts.map +1 -0
  114. package/lib/typescript/capture/install.d.ts +15 -0
  115. package/lib/typescript/capture/install.d.ts.map +1 -0
  116. package/lib/typescript/capture/installExpoImage.d.ts +23 -0
  117. package/lib/typescript/capture/installExpoImage.d.ts.map +1 -0
  118. package/lib/typescript/capture/installRnImage.d.ts +24 -0
  119. package/lib/typescript/capture/installRnImage.d.ts.map +1 -0
  120. package/lib/typescript/capture/overrides.d.ts +83 -0
  121. package/lib/typescript/capture/overrides.d.ts.map +1 -0
  122. package/lib/typescript/capture/savings.d.ts +37 -0
  123. package/lib/typescript/capture/savings.d.ts.map +1 -0
  124. package/lib/typescript/capture/shared.d.ts +61 -0
  125. package/lib/typescript/capture/shared.d.ts.map +1 -0
  126. package/lib/typescript/components/ImageDetail.d.ts +13 -0
  127. package/lib/typescript/components/ImageDetail.d.ts.map +1 -0
  128. package/lib/typescript/components/ImageDetailFooter.d.ts +19 -0
  129. package/lib/typescript/components/ImageDetailFooter.d.ts.map +1 -0
  130. package/lib/typescript/components/ImageRow.d.ts +15 -0
  131. package/lib/typescript/components/ImageRow.d.ts.map +1 -0
  132. package/lib/typescript/components/ImagesHeader.d.ts +25 -0
  133. package/lib/typescript/components/ImagesHeader.d.ts.map +1 -0
  134. package/lib/typescript/components/ImagesIcon.d.ts +13 -0
  135. package/lib/typescript/components/ImagesIcon.d.ts.map +1 -0
  136. package/lib/typescript/components/ImagesListFooter.d.ts +8 -0
  137. package/lib/typescript/components/ImagesListFooter.d.ts.map +1 -0
  138. package/lib/typescript/components/ImagesModal.d.ts +17 -0
  139. package/lib/typescript/components/ImagesModal.d.ts.map +1 -0
  140. package/lib/typescript/components/ImagesRoot.d.ts +11 -0
  141. package/lib/typescript/components/ImagesRoot.d.ts.map +1 -0
  142. package/lib/typescript/components/InsightsBar.d.ts +13 -0
  143. package/lib/typescript/components/InsightsBar.d.ts.map +1 -0
  144. package/lib/typescript/export.d.ts +12 -0
  145. package/lib/typescript/export.d.ts.map +1 -0
  146. package/lib/typescript/format.d.ts +26 -0
  147. package/lib/typescript/format.d.ts.map +1 -0
  148. package/lib/typescript/hooks/useImageRecords.d.ts +11 -0
  149. package/lib/typescript/hooks/useImageRecords.d.ts.map +1 -0
  150. package/lib/typescript/index.d.ts +33 -0
  151. package/lib/typescript/index.d.ts.map +1 -0
  152. package/lib/typescript/preset.d.ts +38 -0
  153. package/lib/typescript/preset.d.ts.map +1 -0
  154. package/lib/typescript/register.d.ts +15 -0
  155. package/lib/typescript/register.d.ts.map +1 -0
  156. package/lib/typescript/state.d.ts +54 -0
  157. package/lib/typescript/state.d.ts.map +1 -0
  158. package/lib/typescript/store/imageEventStore.d.ts +34 -0
  159. package/lib/typescript/store/imageEventStore.d.ts.map +1 -0
  160. package/lib/typescript/store/insights.d.ts +11 -0
  161. package/lib/typescript/store/insights.d.ts.map +1 -0
  162. package/lib/typescript/sync/imagesSyncAdapter.d.ts +205 -0
  163. package/lib/typescript/sync/imagesSyncAdapter.d.ts.map +1 -0
  164. package/lib/typescript/types.d.ts +121 -0
  165. package/lib/typescript/types.d.ts.map +1 -0
  166. package/package.json +67 -0
  167. package/register.d.ts +1 -0
  168. package/register.js +15 -0
  169. package/src/capture/actions.ts +206 -0
  170. package/src/capture/install.ts +36 -0
  171. package/src/capture/installExpoImage.ts +115 -0
  172. package/src/capture/installRnImage.tsx +133 -0
  173. package/src/capture/overrides.ts +282 -0
  174. package/src/capture/savings.ts +193 -0
  175. package/src/capture/shared.ts +387 -0
  176. package/src/components/ImageDetail.tsx +456 -0
  177. package/src/components/ImageDetailFooter.tsx +270 -0
  178. package/src/components/ImageRow.tsx +148 -0
  179. package/src/components/ImagesHeader.tsx +77 -0
  180. package/src/components/ImagesIcon.tsx +66 -0
  181. package/src/components/ImagesListFooter.tsx +193 -0
  182. package/src/components/ImagesModal.tsx +195 -0
  183. package/src/components/ImagesRoot.tsx +18 -0
  184. package/src/components/InsightsBar.tsx +51 -0
  185. package/src/export.ts +100 -0
  186. package/src/format.ts +85 -0
  187. package/src/hooks/useImageRecords.ts +24 -0
  188. package/src/index.tsx +108 -0
  189. package/src/preset.tsx +35 -0
  190. package/src/register.ts +17 -0
  191. package/src/state.ts +88 -0
  192. package/src/store/imageEventStore.ts +162 -0
  193. package/src/store/insights.ts +66 -0
  194. package/src/sync/imagesSyncAdapter.ts +261 -0
  195. package/src/types.ts +111 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Sync adapter for the Images tool, consumed by @buoy-gg/external-sync's
3
+ * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
4
+ * this package doesn't need a dependency on it — same pattern as js-top's
5
+ * adapter). Exposes the live image registry to the desktop dashboard and the
6
+ * MCP server, plus every action the on-device UI has: reload/retry,
7
+ * simulation overrides (force error/loading/blank/URL swap), locate-flash,
8
+ * global modes, and cache clears.
9
+ */
10
+ import type { NetworkMode } from "../capture/overrides";
11
+ export declare const imagesSyncAdapter: {
12
+ version: number;
13
+ getSnapshot: () => {
14
+ records: {
15
+ id: number;
16
+ lib: import("..").ImageLib;
17
+ uri: string;
18
+ kind: import("..").SourceKind;
19
+ status: import("..").ImageStatus;
20
+ mounted: boolean;
21
+ cache: import("..").CacheVerdict | null;
22
+ cacheSource: "event" | "queryCache" | null;
23
+ ms: number | null;
24
+ intrinsic: {
25
+ width: number;
26
+ height: number;
27
+ } | null;
28
+ layout: {
29
+ width: number;
30
+ height: number;
31
+ } | null;
32
+ neededPx: {
33
+ width: number;
34
+ height: number;
35
+ } | null;
36
+ oversizeFactor: number | null;
37
+ decodedKB: number;
38
+ wastedKB: number;
39
+ progressSeen: boolean;
40
+ bytesTotal: number | null;
41
+ error: string | null;
42
+ errorCode: number | null;
43
+ loadCount: number;
44
+ overrideLabel: string | null;
45
+ hasAltText: boolean | null;
46
+ layoutShifts: number;
47
+ ageMs: number;
48
+ }[];
49
+ stats: import("..").ImageStats;
50
+ captureStatus: import("..").CaptureStatus;
51
+ globalModes: {
52
+ network: NetworkMode;
53
+ blank: boolean;
54
+ };
55
+ insights: import("..").ImageInsights;
56
+ };
57
+ subscribe: (onChange: () => void) => () => void;
58
+ actions: {
59
+ /** List records (newest first). Params: { limit?, status? }. */
60
+ list: (params: unknown) => {
61
+ total: number;
62
+ records: {
63
+ id: number;
64
+ lib: import("..").ImageLib;
65
+ uri: string;
66
+ kind: import("..").SourceKind;
67
+ status: import("..").ImageStatus;
68
+ mounted: boolean;
69
+ cache: import("..").CacheVerdict | null;
70
+ cacheSource: "event" | "queryCache" | null;
71
+ ms: number | null;
72
+ intrinsic: {
73
+ width: number;
74
+ height: number;
75
+ } | null;
76
+ layout: {
77
+ width: number;
78
+ height: number;
79
+ } | null;
80
+ neededPx: {
81
+ width: number;
82
+ height: number;
83
+ } | null;
84
+ oversizeFactor: number | null;
85
+ decodedKB: number;
86
+ wastedKB: number;
87
+ progressSeen: boolean;
88
+ bytesTotal: number | null;
89
+ error: string | null;
90
+ errorCode: number | null;
91
+ loadCount: number;
92
+ overrideLabel: string | null;
93
+ hasAltText: boolean | null;
94
+ layoutShifts: number;
95
+ ageMs: number;
96
+ }[];
97
+ stats: import("..").ImageStats;
98
+ captureStatus: import("..").CaptureStatus;
99
+ globalModes: {
100
+ network: NetworkMode;
101
+ blank: boolean;
102
+ };
103
+ insights: import("..").ImageInsights;
104
+ };
105
+ /** Full detail for one record. Params: { id }. */
106
+ getDetail: (params: unknown) => {
107
+ errorHeaders: Record<string, string> | null;
108
+ id: number;
109
+ lib: import("..").ImageLib;
110
+ uri: string;
111
+ kind: import("..").SourceKind;
112
+ status: import("..").ImageStatus;
113
+ mounted: boolean;
114
+ cache: import("..").CacheVerdict | null;
115
+ cacheSource: "event" | "queryCache" | null;
116
+ ms: number | null;
117
+ intrinsic: {
118
+ width: number;
119
+ height: number;
120
+ } | null;
121
+ layout: {
122
+ width: number;
123
+ height: number;
124
+ } | null;
125
+ neededPx: {
126
+ width: number;
127
+ height: number;
128
+ } | null;
129
+ oversizeFactor: number | null;
130
+ decodedKB: number;
131
+ wastedKB: number;
132
+ progressSeen: boolean;
133
+ bytesTotal: number | null;
134
+ error: string | null;
135
+ errorCode: number | null;
136
+ loadCount: number;
137
+ overrideLabel: string | null;
138
+ hasAltText: boolean | null;
139
+ layoutShifts: number;
140
+ ageMs: number;
141
+ };
142
+ /** Bypass-cache reload. Params: { id }. */
143
+ hardReload: (params: unknown) => Promise<import("..").ActionResult>;
144
+ /** Re-encode at displayed size as WebP, return real byte savings. Params: { id }. */
145
+ proveSavings: (params: unknown) => Promise<import("..").SavingsResult>;
146
+ /** Plain fresh load attempt. Params: { id }. */
147
+ retry: (params: unknown) => Promise<import("..").ActionResult>;
148
+ /** Evict the expo-image disk entry for this record. Params: { id }. */
149
+ evictDisk: (params: unknown) => Promise<{
150
+ ok: boolean;
151
+ message: string;
152
+ }>;
153
+ /** Flash a red border on the on-screen image. Params: { id }. */
154
+ flash: (params: unknown) => {
155
+ ok: boolean;
156
+ message: string;
157
+ };
158
+ /**
159
+ * Apply a simulation override. Params: { id, kind, uri? } where kind is
160
+ * 'error' | 'hang' | 'blank' | 'url' ('url' requires uri).
161
+ */
162
+ setOverride: (params: unknown) => {
163
+ ok: boolean;
164
+ message: string;
165
+ };
166
+ /** Remove a simulation override. Params: { id }. */
167
+ clearOverride: (params: unknown) => {
168
+ ok: boolean;
169
+ message: string;
170
+ };
171
+ /**
172
+ * Mass action on every mounted image. Params: { kind } where kind is
173
+ * 'error' | 'loading' | 'blank' | 'reload' | 'flash' | 'restore'.
174
+ */
175
+ massAction: (params: unknown) => Promise<import("..").ActionResult>;
176
+ /** Global network simulation. Params: { mode: 'normal'|'offline'|'cold' }. */
177
+ setNetworkMode: (params: unknown) => {
178
+ ok: boolean;
179
+ modes: {
180
+ network: NetworkMode;
181
+ blank: boolean;
182
+ };
183
+ };
184
+ /** Chrome-style disable-images. Params: { enabled: boolean }. */
185
+ setBlankImages: (params: unknown) => {
186
+ ok: boolean;
187
+ modes: {
188
+ network: NetworkMode;
189
+ blank: boolean;
190
+ };
191
+ };
192
+ /** Clear the captured registry (keeps in-flight loads). */
193
+ clearRecords: () => {
194
+ ok: boolean;
195
+ message: string;
196
+ };
197
+ /** Clear expo-image caches. Params: { memory?, disk? } (default both). */
198
+ clearExpoCaches: (params: unknown) => Promise<{
199
+ ok: boolean;
200
+ message: string;
201
+ }>;
202
+ getCaptureStatus: () => import("..").CaptureStatus;
203
+ };
204
+ };
205
+ //# sourceMappingURL=imagesSyncAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imagesSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/imagesSyncAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsBH,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,sBAAsB,CAAC;AAyExE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAMN,MAAM,IAAI;;QAE9B,gEAAgE;uBACjD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAatB,kDAAkD;4BAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAI3B,2CAA2C;6BACtB,OAAO;QAC5B,qFAAqF;+BAC9D,OAAO;QAC9B,gDAAgD;wBAChC,OAAO;QACvB,uEAAuE;4BAC7C,OAAO;;;;QAUjC,iEAAiE;wBACjD,OAAO;;;;QAIvB;;;WAGG;8BACmB,OAAO;;;;QAwB7B,oDAAoD;gCAC5B,OAAO;;;;QAI/B;;;WAGG;6BACwB,OAAO;QA6BlC,8EAA8E;iCACrD,OAAO;;;;;;;QAQhC,iEAAiE;iCACxC,OAAO;;;;;;;QAIhC,2DAA2D;;;;;QAK3D,0EAA0E;kCAC1C,OAAO;;;;;;CAsB1C,CAAC"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Shared types for the Images tool.
3
+ *
4
+ * One ImageRecord = one load lifecycle of one mounted image instance for one
5
+ * source URI. If the same component instance switches to a new URI, the old
6
+ * record is finalized and a new one starts.
7
+ */
8
+ /** Which image library rendered the instance. */
9
+ export type ImageLib = "rn" | "expo";
10
+ /** Coarse classification of where the source points. */
11
+ export type SourceKind = "network" | "asset" | "file" | "data" | "other";
12
+ export type ImageStatus = "pending" | "loading" | "loaded" | "error";
13
+ /**
14
+ * Where the decoded image came from.
15
+ * - expo-image reports this directly on onLoad (`cacheType`).
16
+ * - RN core is classified after load via `Image.queryCache` (iOS can report
17
+ * 'disk/memory'; misses come back as undefined, shown as unknown).
18
+ */
19
+ export type CacheVerdict = "none" | "disk" | "memory" | "disk/memory";
20
+ export interface ImageRecord {
21
+ id: number;
22
+ lib: ImageLib;
23
+ uri: string;
24
+ sourceKind: SourceKind;
25
+ status: ImageStatus;
26
+ /** False once the owning component instance unmounted. */
27
+ mounted: boolean;
28
+ /** ms epoch when the record was created (≈ render of the instance). */
29
+ createdAt: number;
30
+ /** ms epoch of onLoadStart (native began working on the source). */
31
+ startedAt?: number;
32
+ loadedAt?: number;
33
+ /** ms epoch of the last onError (used for out-of-order event defense). */
34
+ erroredAt?: number;
35
+ /** expo-image only: when pixels were actually presented (onDisplay). */
36
+ displayedAt?: number;
37
+ /** startedAt → loadedAt (falls back to createdAt → loadedAt). */
38
+ durationMs?: number;
39
+ /** Decoded bitmap size in physical pixels (from onLoad). */
40
+ intrinsic?: {
41
+ width: number;
42
+ height: number;
43
+ };
44
+ /** Laid-out view size in dp (from injected onLayout). */
45
+ layout?: {
46
+ width: number;
47
+ height: number;
48
+ };
49
+ cacheVerdict?: CacheVerdict;
50
+ cacheVerdictSource?: "event" | "queryCache";
51
+ /** True if byte progress events fired (a real network download happened). */
52
+ progressSeen?: boolean;
53
+ bytesLoaded?: number;
54
+ bytesTotal?: number;
55
+ /** expo-image iOS only (Android reports null). */
56
+ mediaType?: string | null;
57
+ isAnimated?: boolean;
58
+ error?: string;
59
+ /** iOS RN core only: HTTP status code from onError. */
60
+ errorCode?: number;
61
+ /** iOS RN core only: response headers from onError. */
62
+ errorHeaders?: Record<string, string>;
63
+ /** How many load cycles this record has seen (source retries/reloads). */
64
+ loadCount: number;
65
+ /** Set while a simulation override is active (e.g. "Forced error"). */
66
+ overrideLabel?: string;
67
+ /** Whether the component had alt/accessibilityLabel at last render. */
68
+ hasAltText?: boolean;
69
+ /** Times the laid-out size changed AFTER the image loaded (CLS signal). */
70
+ layoutShifts?: number;
71
+ }
72
+ /** Cross-record findings computed from the live registry. */
73
+ export interface ImageInsights {
74
+ /** Same URI loaded by 2+ mounted instances (duplicate decodes). */
75
+ duplicates: {
76
+ uri: string;
77
+ count: number;
78
+ }[];
79
+ /** Records with suspiciously many load cycles (render-thrash reloads). */
80
+ retryStorms: {
81
+ id: number;
82
+ uri: string;
83
+ loadCount: number;
84
+ }[];
85
+ /** >4 RN-core images loading at once (iOS caps concurrent loads at 4). */
86
+ queueSaturated: boolean;
87
+ /** Mounted images missing alt/accessibilityLabel. */
88
+ missingAlt: number;
89
+ /** Images whose layout changed after load (layout-shift suspects). */
90
+ layoutShifters: {
91
+ id: number;
92
+ uri: string;
93
+ shifts: number;
94
+ }[];
95
+ }
96
+ /** Aggregate stats derived from the current buffer. */
97
+ export interface ImageStats {
98
+ total: number;
99
+ loading: number;
100
+ loaded: number;
101
+ errors: number;
102
+ /** Records whose bytes came over the network this session. */
103
+ networkLoads: number;
104
+ /** Σ intrinsic w×h×4 across loaded, still-mounted records. */
105
+ estDecodedBytes: number;
106
+ /** Σ estimated wasted decoded bytes from oversized sources (mounted). */
107
+ estWastedBytes: number;
108
+ }
109
+ /** Capture capability report — what the interceptors managed to hook. */
110
+ export interface CaptureStatus {
111
+ installed: boolean;
112
+ /** RN core decorator landed before Image module evaluation. */
113
+ rnDecoratorActive: boolean;
114
+ /** RN core Image module was already evaluated when we tried (too late). */
115
+ rnDecoratorTooLate: boolean;
116
+ /** expo-image found and prototype-patched. */
117
+ expoPatched: boolean;
118
+ /** expo-image not installed in this app. */
119
+ expoAvailable: boolean;
120
+ }
121
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@buoy-gg/images",
3
+ "version": "7.0.3",
4
+ "description": "Debug every image in your React Native app — live load registry, cache verdicts (memory/disk/network), timings, oversize + wasted-memory checks, and failure diagnosis for RN <Image> and expo-image. Part of Buoy devtools.",
5
+ "main": "lib/commonjs/index",
6
+ "module": "lib/module/index",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index.tsx",
9
+ "source": "src/index.tsx",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "register.js",
14
+ "register.d.ts"
15
+ ],
16
+ "sideEffects": [
17
+ "**/register.*",
18
+ "**/index.*"
19
+ ],
20
+ "dependencies": {
21
+ "@buoy-gg/shared-ui": "7.0.3"
22
+ },
23
+ "peerDependencies": {
24
+ "expo-image": "*",
25
+ "react": "*",
26
+ "react-native": "*"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "expo-image": {
30
+ "optional": true
31
+ }
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.1.0",
35
+ "@types/react-native": "^0.73.0",
36
+ "typescript": "~5.8.3"
37
+ },
38
+ "react-native-builder-bob": {
39
+ "source": "src",
40
+ "output": "lib",
41
+ "targets": [
42
+ "commonjs",
43
+ "module",
44
+ "typescript"
45
+ ]
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/LovesWorking/react-native-buoy.git",
50
+ "directory": "packages/images"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/LovesWorking/react-native-buoy/issues"
54
+ },
55
+ "homepage": "https://github.com/LovesWorking/react-native-buoy/tree/main/packages/images#readme",
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "tag": "latest"
59
+ },
60
+ "scripts": {
61
+ "build": "bob build",
62
+ "typecheck": "tsc --noEmit",
63
+ "clean": "rimraf lib",
64
+ "test": "pnpm run typecheck",
65
+ "postinstall": "echo \"Run pnpm build to compile this package\""
66
+ }
67
+ }
package/register.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/register.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Subpath shim: `import "@buoy-gg/images/register"`.
3
+ *
4
+ * Installs image capture on import. Must be the FIRST import of the app entry
5
+ * file so the RN-core <Image> decorator lands before React Native's Image
6
+ * module evaluates. Works with and without package `exports` resolution
7
+ * (e.g. Metro `unstable_enablePackageExports: false`).
8
+ *
9
+ * Resolves to src (Metro transpiles TS in node_modules) so it shares ONE
10
+ * module instance with the package root import, which also resolves to src
11
+ * via the "react-native" main field — a lib-pointing shim would create a
12
+ * second copy of the capture layer and split the decorator from the store.
13
+ * Capture state is additionally globalThis-keyed as a second line of defense.
14
+ */
15
+ module.exports = require("./src/register");
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Per-image actions: hard reload (bypass cache) and retry.
3
+ *
4
+ * RN core path: the decorator wrapper owns every mounted <Image>'s props, so a
5
+ * reload request re-renders the wrapper with a bumped `key` (native remount →
6
+ * fresh load cycle) and, for cache-busting, `source.cache: 'reload'` injected
7
+ * — VERIFIED supported on BOTH platforms (iOS NSURLRequest policy;
8
+ * Android ReactImageView.kt reads source."cache" → Fresco full-fetch). The
9
+ * override stays applied until the load settles, then reverts lazily on the
10
+ * app's next natural re-render (avoids an extra cache round-trip).
11
+ *
12
+ * expo-image path: the patched class instance exposes the public
13
+ * `reloadAsync()`. Cache-busting first evicts the disk entry (cache path +
14
+ * expo-file-system delete) and clears expo's memory cache (global — no
15
+ * per-entry memory eviction exists), so the reload is a true refetch that
16
+ * rewrites both cache tiers.
17
+ */
18
+
19
+ import { imagesState } from "../state";
20
+ import { touchRecord } from "./overrides";
21
+ import type { ImageRecord } from "../types";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // RN reload-request bus (consumed by the decorator wrapper)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export function subscribeActions(listener: () => void): () => void {
28
+ const s = imagesState();
29
+ s.actionListeners.add(listener);
30
+ return () => imagesState().actionListeners.delete(listener);
31
+ }
32
+
33
+ export function getReloadRequest(
34
+ recordId: number | null
35
+ ): { nonce: number; bust: boolean } | undefined {
36
+ if (recordId == null) return undefined;
37
+ return imagesState().reloadRequests.get(recordId);
38
+ }
39
+
40
+ /** Reload nonce for a record (0 = never requested). Snapshot for the wrapper. */
41
+ export function getReloadNonce(recordId: number | null): number {
42
+ return getReloadRequest(recordId)?.nonce ?? 0;
43
+ }
44
+
45
+ function requestRnReload(recordId: number, bust: boolean): void {
46
+ const s = imagesState();
47
+ const prev = s.reloadRequests.get(recordId);
48
+ s.reloadRequests.set(recordId, { nonce: (prev?.nonce ?? 0) + 1, bust });
49
+ // touchRecord bumps the render version (the wrapper's snapshot) and emits.
50
+ touchRecord(recordId);
51
+ }
52
+
53
+ /**
54
+ * Mark a request consumed once its load settles (called from the tracker on
55
+ * load/error). Silent — the wrapper reverts the prop override on the app's
56
+ * next natural re-render instead of forcing an immediate one.
57
+ */
58
+ export function settleReloadRequest(recordId: number): void {
59
+ const s = imagesState();
60
+ const req = s.reloadRequests.get(recordId);
61
+ if (req?.bust) {
62
+ s.reloadRequests.set(recordId, { nonce: req.nonce, bust: false });
63
+ }
64
+ }
65
+
66
+ export function dropActionState(recordId: number): void {
67
+ const s = imagesState();
68
+ s.reloadRequests.delete(recordId);
69
+ s.expoInstances.delete(recordId);
70
+ s.overrides.delete(recordId);
71
+ s.flashUntil.delete(recordId);
72
+ s.recordVersions.delete(recordId);
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // expo instance registry (populated by the render patch)
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export function registerExpoInstance(recordId: number | null, instance: unknown): void {
80
+ if (recordId == null) return;
81
+ imagesState().expoInstances.set(recordId, instance);
82
+ }
83
+
84
+ interface ExpoImageStatics {
85
+ getCachePathAsync?: (cacheKey: string) => Promise<string | null>;
86
+ clearMemoryCache?: () => Promise<boolean>;
87
+ }
88
+
89
+ function expoImageStatics(): ExpoImageStatics | null {
90
+ try {
91
+ return require("expo-image").Image ?? null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ interface FileSystemModule {
98
+ deleteAsync?: (uri: string, options?: { idempotent?: boolean }) => Promise<void>;
99
+ }
100
+
101
+ /**
102
+ * expo-file-system, legacy path first (v19+/SDK 54+ moved the function API
103
+ * there) then the main import — same pattern as shared-ui's persistentStorage.
104
+ */
105
+ function loadFileSystem(): FileSystemModule | null {
106
+ try {
107
+ const mod = require("expo-file-system/legacy");
108
+ const fs = mod?.default || mod;
109
+ if (fs?.deleteAsync) return fs;
110
+ } catch {
111
+ // try main import
112
+ }
113
+ try {
114
+ const mod = require("expo-file-system");
115
+ const fs = mod?.default || mod;
116
+ if (fs?.deleteAsync) return fs;
117
+ } catch {
118
+ // not installed
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Delete an expo-image disk-cache entry (JS-only: cache path + file delete).
125
+ * Returns true if an entry existed and was deleted.
126
+ */
127
+ export async function evictExpoDiskEntry(record: ImageRecord): Promise<boolean> {
128
+ const statics = expoImageStatics();
129
+ const fileSystem = loadFileSystem();
130
+ if (!statics?.getCachePathAsync || !fileSystem?.deleteAsync || !record.uri) return false;
131
+ try {
132
+ const path = await statics.getCachePathAsync(record.uri);
133
+ if (!path) return false;
134
+ const uri = path.startsWith("file://") ? path : `file://${path}`;
135
+ await fileSystem.deleteAsync(uri, { idempotent: true });
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // High-level dispatchers (used by the detail view)
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export type ActionResult = { ok: boolean; message: string };
147
+
148
+ /** Fresh load attempt for a mounted image (no cache bypass). */
149
+ export async function retryRecord(record: ImageRecord): Promise<ActionResult> {
150
+ if (!record.mounted) {
151
+ return { ok: false, message: "Instance unmounted — cannot reload" };
152
+ }
153
+ if (record.lib === "expo") {
154
+ const instance = imagesState().expoInstances.get(record.id) as
155
+ | { reloadAsync?: () => Promise<void> }
156
+ | undefined;
157
+ if (!instance?.reloadAsync) {
158
+ return { ok: false, message: "Live instance not reachable" };
159
+ }
160
+ await instance.reloadAsync();
161
+ return { ok: true, message: "Reload requested" };
162
+ }
163
+ requestRnReload(record.id, false);
164
+ return { ok: true, message: "Remounting image" };
165
+ }
166
+
167
+ /** Hard-reload every mounted image (mass cache-bust). Returns count started. */
168
+ export async function hardReloadAllMounted(): Promise<ActionResult> {
169
+ const { getSnapshot } = require("../store/imageEventStore") as {
170
+ getSnapshot: () => ImageRecord[];
171
+ };
172
+ let count = 0;
173
+ for (const record of getSnapshot()) {
174
+ if (!record.mounted) continue;
175
+ const result = await hardReloadRecord(record);
176
+ if (result.ok) count++;
177
+ }
178
+ return { ok: count > 0, message: `Hard-reloading ${count} mounted image${count === 1 ? "" : "s"}` };
179
+ }
180
+
181
+ /** Bypass-cache reload: refetches from network and rewrites the caches. */
182
+ export async function hardReloadRecord(record: ImageRecord): Promise<ActionResult> {
183
+ if (!record.mounted) {
184
+ return { ok: false, message: "Instance unmounted — cannot reload" };
185
+ }
186
+ if (record.sourceKind !== "network") {
187
+ // Local/asset sources have no HTTP cache to bust — plain remount instead.
188
+ return retryRecord(record);
189
+ }
190
+ if (record.lib === "expo") {
191
+ const evicted = await evictExpoDiskEntry(record);
192
+ const statics = expoImageStatics();
193
+ // No per-entry memory eviction exists — global clear is the honest option.
194
+ await statics?.clearMemoryCache?.().catch?.(() => false);
195
+ const result = await retryRecord(record);
196
+ if (!result.ok) return result;
197
+ return {
198
+ ok: true,
199
+ message: evicted
200
+ ? "Disk entry evicted, memory cache cleared, refetching"
201
+ : "Memory cache cleared, refetching (no disk entry found)",
202
+ };
203
+ }
204
+ requestRnReload(record.id, true);
205
+ return { ok: true, message: "Refetching with cache bypassed" };
206
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * installImageCapture — idempotent entry point for the capture layer.
3
+ *
4
+ * IMPORTANT (timing): the RN-core decorator only takes effect if this runs
5
+ * before anything evaluates the `Image` module from 'react-native'. The
6
+ * recommended integration is `import "@buoy-gg/images/register"` as the FIRST
7
+ * import of the app entry file. The expo-image patch has no such constraint.
8
+ *
9
+ * Idempotency is keyed on the globalThis singleton, so even if this package
10
+ * gets evaluated as two module instances, capture installs exactly once.
11
+ */
12
+
13
+ import { imagesState } from "../state";
14
+ import { installExpoImageInterceptor, expoCaptureStatus } from "./installExpoImage";
15
+ import { installRnImageInterceptor, rnCaptureStatus } from "./installRnImage";
16
+ import type { CaptureStatus } from "../types";
17
+
18
+ export function installImageCapture(): void {
19
+ const s = imagesState();
20
+ if (s.captureInstalled) return;
21
+ s.captureInstalled = true;
22
+ installRnImageInterceptor();
23
+ installExpoImageInterceptor();
24
+ }
25
+
26
+ export function getCaptureStatus(): CaptureStatus {
27
+ const rn = rnCaptureStatus();
28
+ const expo = expoCaptureStatus();
29
+ return {
30
+ installed: imagesState().captureInstalled,
31
+ rnDecoratorActive: rn.decoratorActive,
32
+ rnDecoratorTooLate: rn.tooLate,
33
+ expoPatched: expo.patched,
34
+ expoAvailable: expo.available,
35
+ };
36
+ }