@buoy-gg/assets 7.0.29 → 7.0.31

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 (95) hide show
  1. package/lib/commonjs/components/AssetDetail.js +10 -1
  2. package/lib/commonjs/components/AssetDetail.js.map +1 -1
  3. package/lib/commonjs/components/AssetRow.js +13 -4
  4. package/lib/commonjs/components/AssetRow.js.map +1 -1
  5. package/lib/commonjs/components/AssetsFilterBar.js +4 -0
  6. package/lib/commonjs/components/AssetsFilterBar.js.map +1 -1
  7. package/lib/commonjs/components/AssetsHeader.js +11 -5
  8. package/lib/commonjs/components/AssetsHeader.js.map +1 -1
  9. package/lib/commonjs/components/AssetsListControls.js +5 -3
  10. package/lib/commonjs/components/AssetsListControls.js.map +1 -1
  11. package/lib/commonjs/components/AssetsModal.js +10 -0
  12. package/lib/commonjs/components/AssetsModal.js.map +1 -1
  13. package/lib/commonjs/components/BaselineBar.js +5 -0
  14. package/lib/commonjs/components/BaselineBar.js.map +1 -1
  15. package/lib/commonjs/index.js +53 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/store/assetStore.js +8 -1
  18. package/lib/commonjs/store/assetStore.js.map +1 -1
  19. package/lib/commonjs/store/baseline.js +9 -1
  20. package/lib/commonjs/store/baseline.js.map +1 -1
  21. package/lib/commonjs/store/insights.js +2 -11
  22. package/lib/commonjs/store/insights.js.map +1 -1
  23. package/lib/commonjs/utils/deviceScale.js +50 -0
  24. package/lib/commonjs/utils/deviceScale.js.map +1 -0
  25. package/lib/commonjs/utils/testIDs.js +114 -0
  26. package/lib/commonjs/utils/testIDs.js.map +1 -0
  27. package/lib/module/components/AssetDetail.js +10 -1
  28. package/lib/module/components/AssetDetail.js.map +1 -1
  29. package/lib/module/components/AssetRow.js +13 -4
  30. package/lib/module/components/AssetRow.js.map +1 -1
  31. package/lib/module/components/AssetsFilterBar.js +4 -0
  32. package/lib/module/components/AssetsFilterBar.js.map +1 -1
  33. package/lib/module/components/AssetsHeader.js +11 -5
  34. package/lib/module/components/AssetsHeader.js.map +1 -1
  35. package/lib/module/components/AssetsListControls.js +5 -3
  36. package/lib/module/components/AssetsListControls.js.map +1 -1
  37. package/lib/module/components/AssetsModal.js +10 -0
  38. package/lib/module/components/AssetsModal.js.map +1 -1
  39. package/lib/module/components/BaselineBar.js +5 -0
  40. package/lib/module/components/BaselineBar.js.map +1 -1
  41. package/lib/module/index.js +4 -0
  42. package/lib/module/index.js.map +1 -1
  43. package/lib/module/store/assetStore.js +8 -1
  44. package/lib/module/store/assetStore.js.map +1 -1
  45. package/lib/module/store/baseline.js +9 -1
  46. package/lib/module/store/baseline.js.map +1 -1
  47. package/lib/module/store/insights.js +1 -10
  48. package/lib/module/store/insights.js.map +1 -1
  49. package/lib/module/utils/deviceScale.js +46 -0
  50. package/lib/module/utils/deviceScale.js.map +1 -0
  51. package/lib/module/utils/testIDs.js +103 -0
  52. package/lib/module/utils/testIDs.js.map +1 -0
  53. package/lib/typescript/components/AssetDetail.d.ts.map +1 -1
  54. package/lib/typescript/components/AssetRow.d.ts.map +1 -1
  55. package/lib/typescript/components/AssetsFilterBar.d.ts.map +1 -1
  56. package/lib/typescript/components/AssetsHeader.d.ts.map +1 -1
  57. package/lib/typescript/components/AssetsListControls.d.ts.map +1 -1
  58. package/lib/typescript/components/AssetsModal.d.ts.map +1 -1
  59. package/lib/typescript/components/BaselineBar.d.ts.map +1 -1
  60. package/lib/typescript/index.d.ts +1 -0
  61. package/lib/typescript/index.d.ts.map +1 -1
  62. package/lib/typescript/store/assetStore.d.ts +6 -0
  63. package/lib/typescript/store/assetStore.d.ts.map +1 -1
  64. package/lib/typescript/store/baseline.d.ts.map +1 -1
  65. package/lib/typescript/store/insights.d.ts.map +1 -1
  66. package/lib/typescript/utils/deviceScale.d.ts +25 -0
  67. package/lib/typescript/utils/deviceScale.d.ts.map +1 -0
  68. package/lib/typescript/utils/testIDs.d.ts +75 -0
  69. package/lib/typescript/utils/testIDs.d.ts.map +1 -0
  70. package/package.json +5 -6
  71. package/src/capture/expo.ts +0 -45
  72. package/src/capture/install.ts +0 -73
  73. package/src/capture/measure.ts +0 -167
  74. package/src/capture/metroGraph.ts +0 -146
  75. package/src/capture/registry.ts +0 -137
  76. package/src/components/AssetDetail.tsx +0 -207
  77. package/src/components/AssetRow.tsx +0 -243
  78. package/src/components/AssetsFilterBar.tsx +0 -99
  79. package/src/components/AssetsHeader.tsx +0 -75
  80. package/src/components/AssetsIcon.tsx +0 -9
  81. package/src/components/AssetsListControls.tsx +0 -101
  82. package/src/components/AssetsModal.tsx +0 -371
  83. package/src/components/BaselineBar.tsx +0 -98
  84. package/src/export.ts +0 -110
  85. package/src/format.ts +0 -53
  86. package/src/hooks/useAssetRecords.ts +0 -26
  87. package/src/index.tsx +0 -87
  88. package/src/preset.tsx +0 -35
  89. package/src/remote.ts +0 -65
  90. package/src/state.ts +0 -84
  91. package/src/store/assetStore.ts +0 -209
  92. package/src/store/baseline.ts +0 -152
  93. package/src/store/insights.ts +0 -145
  94. package/src/sync/assetsSyncAdapter.ts +0 -151
  95. package/src/types.ts +0 -160
@@ -1,371 +0,0 @@
1
- /**
2
- * AssetsModal — bottom-sheet wrapper rendered by FloatingDevTools when the
3
- * Assets tool is opened. Inventory list (largest first) → tap for detail.
4
- * Opening triggers a full rescan (registry walk + Metro graph + size
5
- * measurement); merely installing the package never hits the network.
6
- */
7
-
8
- import { useEffect, useState } from "react";
9
- import { FlatList, StyleSheet, Text, View } from "react-native";
10
- import { EventStepperFooter, JsModal, macOSColors, night } from "@buoy-gg/shared-ui";
11
- import type { ModalState } from "@buoy-gg/shared-ui";
12
- import { getScanStatus, installAssetCapture, rescan } from "../capture/install";
13
- import { formatAssetMarkdown, formatInventoryMarkdown } from "../export";
14
- import { useAssetRecords } from "../hooks/useAssetRecords";
15
- import { remoteAssetsDispatch } from "../remote";
16
- import { clearRecords, estDecodedBytes, getRecord } from "../store/assetStore";
17
- import { getDiff, loadBaseline } from "../store/baseline";
18
- import { AssetDetail } from "./AssetDetail";
19
- import { AssetRow } from "./AssetRow";
20
- import { AssetsFilterBar, type AssetFilter } from "./AssetsFilterBar";
21
- import { AssetsHeader } from "./AssetsHeader";
22
- import { AssetsListControls, type AssetSort } from "./AssetsListControls";
23
- import { BaselineBar } from "./BaselineBar";
24
- import type { AssetRecord } from "../types";
25
-
26
- interface AssetsModalProps {
27
- visible: boolean;
28
- onClose: () => void;
29
- onBack?: () => void;
30
- onMinimize?: (modalState: ModalState) => void;
31
- persistenceKey?: string;
32
- /** Render as a plain header-bar + body stack instead of a JsModal (desktop). */
33
- inline?: boolean;
34
- }
35
-
36
- function ScanNotice() {
37
- const status = getScanStatus();
38
- const notices: string[] = [];
39
- if (!status.registryAvailable) {
40
- notices.push(
41
- "Asset registry unavailable — this environment doesn't expose @react-native/assets-registry."
42
- );
43
- } else if (!status.graphSupported && status.graphError) {
44
- notices.push(
45
- "No Metro dev server — showing runtime-registered assets only. The full bundle inventory and byte sizes need a dev build."
46
- );
47
- }
48
- if (status.measuring) notices.push("Measuring sizes from the dev server…");
49
- if (notices.length === 0) return null;
50
- return (
51
- <View style={styles.notice}>
52
- {notices.map((n) => (
53
- <Text key={n} style={styles.noticeText}>
54
- {n}
55
- </Text>
56
- ))}
57
- </View>
58
- );
59
- }
60
-
61
- function FontFamilies() {
62
- const families = getScanStatus().fontFamilies;
63
- if (!families.length) return null;
64
- return (
65
- <View style={styles.fontsBox}>
66
- <Text style={styles.fontsTitle}>LOADED FONT FAMILIES ({families.length})</Text>
67
- <View style={styles.fontsWrap}>
68
- {families.map((family) => (
69
- <Text key={family} style={styles.fontChip}>
70
- {family}
71
- </Text>
72
- ))}
73
- </View>
74
- </View>
75
- );
76
- }
77
-
78
- function EmptyState() {
79
- return (
80
- <View style={styles.empty}>
81
- <Text style={styles.emptyTitle}>No assets found yet</Text>
82
- <Text style={styles.emptyText}>
83
- Every bundled asset — images, fonts, video, audio — appears here with
84
- sizes, scale coverage, and never-loaded detection. Try Rescan after
85
- navigating around the app.
86
- </Text>
87
- </View>
88
- );
89
- }
90
-
91
- /** Largest first — measured bytes, falling back to decoded estimate. */
92
- function bySizeDesc(a: AssetRecord, b: AssetRecord): number {
93
- return (b.sizeBytes ?? estDecodedBytes(b)) - (a.sizeBytes ?? estDecodedBytes(a));
94
- }
95
-
96
- const COMPARATORS: Record<AssetSort, (a: AssetRecord, b: AssetRecord) => number> = {
97
- size: bySizeDesc,
98
- name: (a, b) => a.name.localeCompare(b.name) || a.type.localeCompare(b.type),
99
- type: (a, b) => a.type.localeCompare(b.type) || bySizeDesc(a, b),
100
- };
101
-
102
- export function AssetsModal({
103
- visible,
104
- onClose,
105
- onBack,
106
- onMinimize,
107
- persistenceKey,
108
- inline = false,
109
- }: AssetsModalProps) {
110
- // Safety net: the index import normally installed this already.
111
- installAssetCapture();
112
-
113
- const { records, stats, insights } = useAssetRecords();
114
- const [selectedId, setSelectedId] = useState<number | null>(null);
115
- const [filter, setFilter] = useState<AssetFilter>("all");
116
- const [query, setQuery] = useState("");
117
- const [sort, setSort] = useState<AssetSort>("size");
118
- const selected = selectedId != null ? getRecord(selectedId) : undefined;
119
-
120
- // Full scan on open — locally on device, forwarded on dashboard mirrors.
121
- // The baseline load is async and re-renders through the store emit.
122
- useEffect(() => {
123
- if (!visible) return;
124
- void loadBaseline();
125
- const remote = remoteAssetsDispatch();
126
- if (remote) {
127
- remote("rescan").catch(() => {});
128
- } else {
129
- void rescan();
130
- }
131
- }, [visible]);
132
-
133
- const sorted = [...records].sort(COMPARATORS[sort]);
134
- const trimmedQuery = query.trim().toLowerCase();
135
- const filtered = sorted.filter((r) => {
136
- if (filter === "unused" && r.loaded) return false;
137
- if (filter !== "all" && filter !== "unused" && r.kind !== filter) return false;
138
- if (trimmedQuery && !`${r.name}.${r.type}`.toLowerCase().includes(trimmedQuery)) {
139
- return false;
140
- }
141
- return true;
142
- });
143
-
144
- const selectedIndex = selected
145
- ? filtered.findIndex((r) => r.id === selected.id)
146
- : -1;
147
- const stepTo = (index: number) => {
148
- const target = filtered[index];
149
- if (target) setSelectedId(target.id);
150
- };
151
-
152
- // Set lookups so each row's badge checks are O(1).
153
- const flagSets = {
154
- scaleGaps: new Set(insights.scaleGaps),
155
- scaleAnomalies: new Set(insights.scaleAnomalies),
156
- duplicates: new Set(insights.duplicates.flatMap((d) => d.ids)),
157
- webpCandidates: new Set(insights.webpCandidates),
158
- overDecode: new Set(insights.overDecode),
159
- heavy: new Set([...insights.heavyGifs, ...insights.heavyFonts]),
160
- };
161
-
162
- const diff = getDiff();
163
-
164
- // The proportional size bar is relative to the largest asset in view.
165
- let maxBytes = 0;
166
- for (const r of filtered) {
167
- const bytes = r.sizeBytes ?? estDecodedBytes(r);
168
- if (bytes > maxBytes) maxBytes = bytes;
169
- }
170
-
171
- const getReport = () =>
172
- selected
173
- ? formatAssetMarkdown(selected)
174
- : formatInventoryMarkdown(sorted, stats, insights, getScanStatus(), getDiff());
175
-
176
- const headerContent = (
177
- <AssetsHeader
178
- stats={stats}
179
- measuring={getScanStatus().measuring}
180
- detailOpen={selected != null}
181
- detailIndex={selectedIndex}
182
- detailTotal={filtered.length}
183
- onCloseDetail={() => setSelectedId(null)}
184
- onClear={clearRecords}
185
- getReport={getReport}
186
- onBack={onBack}
187
- />
188
- );
189
-
190
- const body = selected ? (
191
- <View style={styles.detailContainer}>
192
- <AssetDetail record={selected} insights={insights} />
193
- {/* Same stepper the network tool uses — steps through the FILTERED
194
- list, so search/kind filters re-scope prev/next in place. */}
195
- {selectedIndex >= 0 ? (
196
- <EventStepperFooter
197
- currentIndex={selectedIndex}
198
- totalItems={filtered.length}
199
- itemLabel="Asset"
200
- onPrevious={() => stepTo(selectedIndex - 1)}
201
- onNext={() => stepTo(selectedIndex + 1)}
202
- />
203
- ) : null}
204
- </View>
205
- ) : (
206
- <View style={styles.listContainer}>
207
- <AssetsFilterBar stats={stats} filter={filter} onChange={setFilter} />
208
- <AssetsListControls
209
- query={query}
210
- onQueryChange={setQuery}
211
- sort={sort}
212
- onSortChange={setSort}
213
- showBaselineButton={diff == null}
214
- />
215
- <BaselineBar diff={diff} />
216
- <ScanNotice />
217
- {filter === "font" ? <FontFamilies /> : null}
218
- <FlatList
219
- data={filtered}
220
- keyExtractor={(item: AssetRecord) => String(item.id)}
221
- contentContainerStyle={styles.listContent}
222
- renderItem={({ item }) => (
223
- <AssetRow
224
- record={item}
225
- flags={{
226
- scaleGap: flagSets.scaleGaps.has(item.id),
227
- scaleAnomaly: flagSets.scaleAnomalies.has(item.id),
228
- duplicate: flagSets.duplicates.has(item.id),
229
- webp: flagSets.webpCandidates.has(item.id),
230
- oversize: flagSets.overDecode.has(item.id),
231
- heavy: flagSets.heavy.has(item.id),
232
- }}
233
- sizeFraction={
234
- maxBytes > 0
235
- ? (item.sizeBytes ?? estDecodedBytes(item)) / maxBytes
236
- : 0
237
- }
238
- // Guarded: programmatic invocations (automation) can call this
239
- // with an event object instead of an id.
240
- onPress={(id) => setSelectedId(typeof id === "number" ? id : null)}
241
- />
242
- )}
243
- ListEmptyComponent={<EmptyState />}
244
- />
245
- </View>
246
- );
247
-
248
- // The ONLY difference between phone and desktop is which shell the header
249
- // and body go in (same pattern as ImagesModal).
250
- if (inline) {
251
- return (
252
- <View style={styles.inlineRoot}>
253
- <View style={styles.inlineHeader}>{headerContent}</View>
254
- {body}
255
- </View>
256
- );
257
- }
258
-
259
- return (
260
- <JsModal
261
- visible={visible}
262
- onClose={onClose}
263
- onMinimize={onMinimize}
264
- persistenceKey={persistenceKey}
265
- header={{
266
- showToggleButton: true,
267
- customContent: headerContent,
268
- }}
269
- enablePersistence
270
- initialMode="bottomSheet"
271
- disableScrollWrapper
272
- >
273
- {body}
274
- </JsModal>
275
- );
276
- }
277
-
278
- /**
279
- * The same Assets tool rendered INLINE for hosts that place tools in a panel
280
- * rather than a modal (the desktop dashboard). One implementation only.
281
- */
282
- export function AssetsToolView(
283
- props: Omit<AssetsModalProps, "visible" | "onClose" | "inline"> = {}
284
- ) {
285
- return <AssetsModal {...props} inline visible onClose={() => {}} />;
286
- }
287
-
288
- const styles = StyleSheet.create({
289
- inlineRoot: {
290
- flex: 1,
291
- minHeight: 0,
292
- // Transparent so the desktop workbench's hoisted sky (one field spanning
293
- // sidebar + panel) shows through — the shell paints its base beneath it.
294
- backgroundColor: "transparent",
295
- },
296
- inlineHeader: {
297
- paddingHorizontal: 12,
298
- paddingVertical: 8,
299
- borderBottomWidth: 1,
300
- borderBottomColor: macOSColors.border.default,
301
- // Clear, like Bench — the host's hoisted sky shows through the header.
302
- backgroundColor: "transparent",
303
- },
304
- listContainer: {
305
- flex: 1,
306
- },
307
- listContent: {
308
- paddingBottom: 16,
309
- },
310
- detailContainer: {
311
- flex: 1,
312
- },
313
- notice: {
314
- marginHorizontal: 10,
315
- marginBottom: 8,
316
- padding: 10,
317
- borderRadius: 8,
318
- borderWidth: 1,
319
- borderColor: night.color.border,
320
- backgroundColor: night.color.surface,
321
- gap: 4,
322
- },
323
- noticeText: {
324
- color: night.color.textSecondary,
325
- fontSize: 11,
326
- },
327
- fontsBox: {
328
- marginHorizontal: 10,
329
- marginTop: 8,
330
- padding: 10,
331
- borderRadius: 8,
332
- backgroundColor: night.color.surface,
333
- gap: 6,
334
- },
335
- fontsTitle: {
336
- color: night.color.textTertiary,
337
- fontSize: 9,
338
- fontWeight: "700",
339
- letterSpacing: 0.5,
340
- },
341
- fontsWrap: {
342
- flexDirection: "row",
343
- flexWrap: "wrap",
344
- gap: 6,
345
- },
346
- fontChip: {
347
- color: night.color.textSecondary,
348
- fontSize: 10,
349
- backgroundColor: night.color.bg,
350
- borderRadius: 999,
351
- paddingHorizontal: 8,
352
- paddingVertical: 3,
353
- overflow: "hidden",
354
- },
355
- empty: {
356
- alignItems: "center",
357
- padding: 32,
358
- gap: 8,
359
- },
360
- emptyTitle: {
361
- color: night.color.text,
362
- fontSize: 13,
363
- fontWeight: "600",
364
- },
365
- emptyText: {
366
- color: night.color.textSecondary,
367
- fontSize: 11,
368
- textAlign: "center",
369
- lineHeight: 16,
370
- },
371
- });
@@ -1,98 +0,0 @@
1
- /**
2
- * BaselineBar — save/compare-against-baseline controls.
3
- *
4
- * No baseline: one "SAVE BASELINE" button. With a baseline: the diff summary
5
- * ("+3 new · 1 removed · +1.2 MB since 2h ago") plus UPDATE and ✕ (clear).
6
- * Growth renders in warning color, shrinkage in success — that's the whole
7
- * point of the feature.
8
- */
9
-
10
- import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
11
- import { macOSColors, night } from "@buoy-gg/shared-ui";
12
- import { formatBytes } from "../format";
13
- import { clearBaseline, saveBaseline } from "../store/baseline";
14
- import type { AssetsDiff } from "../types";
15
-
16
- function formatAge(savedAt: number): string {
17
- const minutes = Math.max(1, Math.round((Date.now() - savedAt) / 60000));
18
- if (minutes < 60) return `${minutes}m ago`;
19
- const hours = Math.round(minutes / 60);
20
- if (hours < 48) return `${hours}h ago`;
21
- return `${Math.round(hours / 24)}d ago`;
22
- }
23
-
24
- export function BaselineBar({ diff }: { diff: AssetsDiff | null }) {
25
- // No baseline → no bar; the BASE button in AssetsListControls creates one.
26
- if (!diff) return null;
27
-
28
- const parts: string[] = [];
29
- if (diff.added.length) parts.push(`+${diff.added.length} new`);
30
- if (diff.removed.length) parts.push(`${diff.removed.length} removed`);
31
- if (diff.grown.length) parts.push(`${diff.grown.length} grew`);
32
- const net =
33
- diff.netBytes != null && Math.abs(diff.netBytes) >= 1024
34
- ? `${diff.netBytes > 0 ? "+" : "−"}${formatBytes(Math.abs(diff.netBytes))}`
35
- : null;
36
- const changed = parts.length > 0 || net != null;
37
- const netColor =
38
- diff.netBytes != null && diff.netBytes > 0
39
- ? macOSColors.semantic.warning
40
- : macOSColors.semantic.success;
41
-
42
- return (
43
- <View style={styles.bar}>
44
- <Text style={styles.summary} numberOfLines={1}>
45
- {changed ? parts.join(" · ") : "No changes"}
46
- {net ? " · " : ""}
47
- {net ? <Text style={{ color: netColor }}>{net}</Text> : null}
48
- <Text style={styles.age}> vs {formatAge(diff.baselineSavedAt)}</Text>
49
- </Text>
50
- <TouchableOpacity style={styles.button} onPress={() => void saveBaseline()}>
51
- <Text style={styles.buttonText}>UPDATE</Text>
52
- </TouchableOpacity>
53
- <TouchableOpacity style={styles.button} onPress={() => void clearBaseline()}>
54
- <Text style={styles.buttonText}>✕</Text>
55
- </TouchableOpacity>
56
- </View>
57
- );
58
- }
59
-
60
- const styles = StyleSheet.create({
61
- bar: {
62
- flexDirection: "row",
63
- alignItems: "center",
64
- gap: 8,
65
- marginHorizontal: 10,
66
- marginBottom: 8,
67
- paddingHorizontal: 10,
68
- paddingVertical: 6,
69
- borderWidth: 1,
70
- borderColor: night.color.border,
71
- borderRadius: 8,
72
- backgroundColor: night.color.surface,
73
- },
74
- summary: {
75
- flex: 1,
76
- color: night.color.textSecondary,
77
- fontSize: 10,
78
- fontWeight: "600",
79
- },
80
- age: {
81
- color: night.color.textTertiary,
82
- fontWeight: "400",
83
- },
84
- button: {
85
- backgroundColor: night.color.surfaceElevated,
86
- borderWidth: 1,
87
- borderColor: night.color.border,
88
- borderRadius: 6,
89
- paddingHorizontal: 8,
90
- paddingVertical: 5,
91
- },
92
- buttonText: {
93
- color: night.color.textSecondary,
94
- fontSize: 9,
95
- fontWeight: "700",
96
- letterSpacing: 0.5,
97
- },
98
- });
package/src/export.ts DELETED
@@ -1,110 +0,0 @@
1
- /**
2
- * Markdown reports for the shared ToolbarCopyButton — the paste-into-your-
3
- * issue-tracker view of the inventory.
4
- */
5
-
6
- import { formatBytes, formatDimensions, formatScales } from "./format";
7
- import { estDecodedBytes } from "./store/assetStore";
8
- import type {
9
- AssetRecord,
10
- AssetsDiff,
11
- AssetsInsights,
12
- AssetsScanStatus,
13
- AssetsStats,
14
- } from "./types";
15
-
16
- export function formatAssetMarkdown(record: AssetRecord): string {
17
- const lines = [
18
- `# Asset: ${record.name}.${record.type}`,
19
- "",
20
- `- Kind: ${record.kind}`,
21
- `- Status: ${record.loaded ? "loaded at runtime" : "in bundle, never loaded"}`,
22
- ];
23
- if (record.kind === "image") lines.push(`- Dimensions: ${formatDimensions(record)} (logical)`);
24
- if (record.scales.length) lines.push(`- Scales: ${formatScales(record.scales)}`);
25
- if (record.sizeBytes != null && record.sizeSource === "measured") {
26
- lines.push(`- Size (all variants): ${formatBytes(record.sizeBytes)}`);
27
- }
28
- const estimate = estDecodedBytes(record);
29
- if (estimate > 0) lines.push(`- Decoded memory: ~${formatBytes(estimate)}`);
30
- if (record.hash) lines.push(`- Hash: ${record.hash}`);
31
- if (record.httpServerLocation) lines.push(`- Location: ${record.httpServerLocation}`);
32
- if (record.fileSystemLocation) lines.push(`- Source dir: ${record.fileSystemLocation}`);
33
- if (record.resolvedUri) lines.push(`- Resolved URI: ${record.resolvedUri}`);
34
- return lines.join("\n");
35
- }
36
-
37
- export function formatInventoryMarkdown(
38
- records: AssetRecord[],
39
- stats: AssetsStats,
40
- insights: AssetsInsights,
41
- scanStatus: AssetsScanStatus,
42
- diff?: AssetsDiff | null
43
- ): string {
44
- const lines = [
45
- "# Asset inventory",
46
- "",
47
- `${stats.total} assets · ${stats.loaded} loaded · ${stats.neverLoaded} never loaded` +
48
- (stats.totalBytes > 0
49
- ? ` · ${formatBytes(stats.totalBytes)} measured (${stats.measuredCount}/${stats.total})`
50
- : ""),
51
- ];
52
- if (scanStatus.fontFamilies.length) {
53
- lines.push(`Fonts loaded: ${scanStatus.fontFamilies.join(", ")}`);
54
- }
55
- if (!scanStatus.graphSupported) {
56
- lines.push(
57
- scanStatus.graphError
58
- ? `_Metro bundle graph unavailable (${scanStatus.graphError}) — runtime-registered assets only._`
59
- : "_No Metro bundle graph — runtime-registered assets only (release build)._"
60
- );
61
- }
62
-
63
- if (diff) {
64
- const net =
65
- diff.netBytes != null
66
- ? `${diff.netBytes >= 0 ? "+" : "−"}${formatBytes(Math.abs(diff.netBytes))} net`
67
- : "net size unknown";
68
- lines.push(
69
- "",
70
- `## Since baseline (${new Date(diff.baselineSavedAt).toISOString()})`,
71
- `- ${diff.added.length} added · ${diff.removed.length} removed · ${diff.grown.length} grew · ${net}`
72
- );
73
- for (const gone of diff.removed.slice(0, 20)) {
74
- lines.push(`- removed: ${gone.name}.${gone.type}`);
75
- }
76
- }
77
-
78
- const findings: string[] = [];
79
- if (insights.neverLoaded.length) findings.push(`- ${insights.neverLoaded.length} shipped but never loaded`);
80
- if (insights.duplicates.length) findings.push(`- ${insights.duplicates.length} duplicate content groups`);
81
- if (insights.webpCandidates.length) findings.push(`- ${insights.webpCandidates.length} WebP conversion candidates`);
82
- if (insights.scaleGaps.length) findings.push(`- ${insights.scaleGaps.length} scale-coverage gaps`);
83
- if (insights.overDecode.length) findings.push(`- ${insights.overDecode.length} images decoding larger than 2× the screen`);
84
- if (insights.heavyGifs.length) findings.push(`- ${insights.heavyGifs.length} heavy GIFs (convert to video/Lottie)`);
85
- if (insights.heavyFonts.length) findings.push(`- ${insights.heavyFonts.length} heavy fonts (subset candidates)`);
86
- if (insights.scaleAnomalies.length) findings.push(`- ${insights.scaleAnomalies.length} scale-variant anomalies (check exports)`);
87
- if (findings.length) lines.push("", "## Findings", ...findings);
88
-
89
- lines.push(
90
- "",
91
- "## Assets (largest first)",
92
- "",
93
- "| Asset | Kind | Dimensions | Scales | Size | Loaded |",
94
- "| --- | --- | --- | --- | --- | --- |"
95
- );
96
- for (const r of records) {
97
- const size =
98
- r.sizeBytes != null && r.sizeSource === "measured"
99
- ? formatBytes(r.sizeBytes)
100
- : estDecodedBytes(r) > 0
101
- ? `~${formatBytes(estDecodedBytes(r))}`
102
- : "—";
103
- lines.push(
104
- `| ${r.name}.${r.type} | ${r.kind} | ${
105
- r.kind === "image" ? formatDimensions(r) : "—"
106
- } | ${formatScales(r.scales)} | ${size} | ${r.loaded ? "yes" : "NO"} |`
107
- );
108
- }
109
- return lines.join("\n");
110
- }
package/src/format.ts DELETED
@@ -1,53 +0,0 @@
1
- /**
2
- * Shared formatting helpers for rows, detail view, and markdown export.
3
- */
4
-
5
- import type { AssetKind, AssetRecord } from "./types";
6
-
7
- export function formatBytes(bytes: number | null | undefined): string {
8
- if (bytes == null || bytes <= 0) return "—";
9
- if (bytes < 1024) return `${bytes} B`;
10
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
11
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
12
- }
13
-
14
- export function formatScales(scales: number[]): string {
15
- const unique = [...new Set(scales)].sort((a, b) => a - b);
16
- if (!unique.length) return "—";
17
- if (unique.length === 1) return `@${unique[0]}x`;
18
- if (unique.length <= 3) return unique.map((s) => `@${s}x`).join(" ");
19
- return `@${unique[0]}x–@${unique[unique.length - 1]}x`;
20
- }
21
-
22
- export function formatDimensions(record: AssetRecord): string {
23
- if (record.width == null || record.height == null) return "—";
24
- return `${Math.round(record.width)}×${Math.round(record.height)}`;
25
- }
26
-
27
- /** Display size: measured bytes, or "~" decoded estimate for unmeasured images. */
28
- export function formatSize(record: AssetRecord, estDecoded: number): string {
29
- if (record.sizeBytes != null && record.sizeSource === "measured") {
30
- return formatBytes(record.sizeBytes);
31
- }
32
- if (estDecoded > 0) return `~${formatBytes(estDecoded)}`;
33
- return "—";
34
- }
35
-
36
- export const KIND_LABELS: Record<AssetKind, string> = {
37
- image: "IMG",
38
- font: "FONT",
39
- video: "VID",
40
- audio: "AUD",
41
- data: "DATA",
42
- other: "MISC",
43
- };
44
-
45
- /** Family accent per kind — the card's left edge, like network's method bar. */
46
- export const KIND_COLORS: Record<AssetKind, string> = {
47
- image: "#2DD4BF",
48
- font: "#C084FC",
49
- video: "#FF9F1C",
50
- audio: "#FF3366",
51
- data: "#00D4FF",
52
- other: "#8E8E93",
53
- };
@@ -1,26 +0,0 @@
1
- /**
2
- * React bindings for the asset store.
3
- */
4
-
5
- import { useMemo, useSyncExternalStore } from "react";
6
- import { computeStats, getSnapshot, subscribe } from "../store/assetStore";
7
- import { computeInsights } from "../store/insights";
8
- import type { AssetRecord, AssetsInsights, AssetsStats } from "../types";
9
-
10
- export interface UseAssetRecordsResult {
11
- records: AssetRecord[];
12
- stats: AssetsStats;
13
- insights: AssetsInsights;
14
- }
15
-
16
- export function useAssetRecords(): UseAssetRecordsResult {
17
- const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
18
- return useMemo(
19
- () => ({
20
- records: snapshot,
21
- stats: computeStats(snapshot),
22
- insights: computeInsights(snapshot),
23
- }),
24
- [snapshot]
25
- );
26
- }