@buoy-gg/assets 7.0.29 → 7.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/AssetRow.js +13 -4
- package/lib/commonjs/components/AssetRow.js.map +1 -1
- package/lib/commonjs/utils/testIDs.js +114 -0
- package/lib/commonjs/utils/testIDs.js.map +1 -0
- package/lib/module/components/AssetRow.js +13 -4
- package/lib/module/components/AssetRow.js.map +1 -1
- package/lib/module/utils/testIDs.js +103 -0
- package/lib/module/utils/testIDs.js.map +1 -0
- package/lib/typescript/components/AssetRow.d.ts.map +1 -1
- package/lib/typescript/utils/testIDs.d.ts +75 -0
- package/lib/typescript/utils/testIDs.d.ts.map +1 -0
- package/package.json +5 -6
- package/src/capture/expo.ts +0 -45
- package/src/capture/install.ts +0 -73
- package/src/capture/measure.ts +0 -167
- package/src/capture/metroGraph.ts +0 -146
- package/src/capture/registry.ts +0 -137
- package/src/components/AssetDetail.tsx +0 -207
- package/src/components/AssetRow.tsx +0 -243
- package/src/components/AssetsFilterBar.tsx +0 -99
- package/src/components/AssetsHeader.tsx +0 -75
- package/src/components/AssetsIcon.tsx +0 -9
- package/src/components/AssetsListControls.tsx +0 -101
- package/src/components/AssetsModal.tsx +0 -371
- package/src/components/BaselineBar.tsx +0 -98
- package/src/export.ts +0 -110
- package/src/format.ts +0 -53
- package/src/hooks/useAssetRecords.ts +0 -26
- package/src/index.tsx +0 -87
- package/src/preset.tsx +0 -35
- package/src/remote.ts +0 -65
- package/src/state.ts +0 -84
- package/src/store/assetStore.ts +0 -209
- package/src/store/baseline.ts +0 -152
- package/src/store/insights.ts +0 -145
- package/src/sync/assetsSyncAdapter.ts +0 -151
- package/src/types.ts +0 -160
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
|
-
}
|
package/src/index.tsx
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @buoy-gg/assets — bundled-asset inventory for React Native / Expo.
|
|
3
|
-
*
|
|
4
|
-
* Enumerates every asset in the app (images, fonts, video, audio, …) via the
|
|
5
|
-
* runtime asset registry, enriched in dev with the Metro graph (full-bundle
|
|
6
|
-
* coverage, byte sizes, never-loaded detection) and on Expo with loaded font
|
|
7
|
-
* families + the updates localAssets map. Zero configuration: importing this
|
|
8
|
-
* package installs the (cheap, synchronous) registry capture.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Tool preset (FloatingDevTools)
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
export { assetsToolPreset, createAssetsTool } from "./preset";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Components
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
export { AssetsModal, AssetsToolView } from "./components/AssetsModal";
|
|
20
|
-
export { AssetsIcon, ASSETS_ICON_COLOR } from "./components/AssetsIcon";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Capture / scanning
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
export { installAssetCapture, rescan, getScanStatus } from "./capture/install";
|
|
26
|
-
export { measureSizes } from "./capture/measure";
|
|
27
|
-
export { fetchAssetGraph, getDevServerOrigin } from "./capture/metroGraph";
|
|
28
|
-
export { getLoadedFontFamilies, getLocalAssets } from "./capture/expo";
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Store
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
export {
|
|
34
|
-
clearRecords,
|
|
35
|
-
computeStats,
|
|
36
|
-
estDecodedBytes,
|
|
37
|
-
getRecord,
|
|
38
|
-
getSnapshot,
|
|
39
|
-
subscribe,
|
|
40
|
-
} from "./store/assetStore";
|
|
41
|
-
export { computeInsights } from "./store/insights";
|
|
42
|
-
export {
|
|
43
|
-
clearBaseline,
|
|
44
|
-
computeDiff,
|
|
45
|
-
getDiff,
|
|
46
|
-
loadBaseline,
|
|
47
|
-
saveBaseline,
|
|
48
|
-
} from "./store/baseline";
|
|
49
|
-
export { useAssetRecords } from "./hooks/useAssetRecords";
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Reports
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
export { formatAssetMarkdown, formatInventoryMarkdown } from "./export";
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// External sync (desktop dashboard + MCP)
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
export { assetsSyncAdapter } from "./sync/assetsSyncAdapter";
|
|
60
|
-
export {
|
|
61
|
-
disableAssetsCapture,
|
|
62
|
-
replaceAssetsFromSnapshot,
|
|
63
|
-
setRemoteAssetsHandler,
|
|
64
|
-
} from "./remote";
|
|
65
|
-
export type { AssetsRemoteDispatch } from "./remote";
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Types
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
export type {
|
|
71
|
-
AssetKind,
|
|
72
|
-
AssetOrigin,
|
|
73
|
-
AssetRecord,
|
|
74
|
-
AssetRecordWire,
|
|
75
|
-
AssetsBaseline,
|
|
76
|
-
AssetsDiff,
|
|
77
|
-
AssetsInsights,
|
|
78
|
-
AssetsScanStatus,
|
|
79
|
-
AssetsSnapshotPayload,
|
|
80
|
-
AssetsStats,
|
|
81
|
-
DuplicateInsight,
|
|
82
|
-
} from "./types";
|
|
83
|
-
|
|
84
|
-
// Side effect: install the registry capture at import (walk + patch — cheap,
|
|
85
|
-
// synchronous, no network). The Metro graph fetch waits for the modal/action.
|
|
86
|
-
import { installAssetCapture as installOnImport } from "./capture/install";
|
|
87
|
-
installOnImport();
|
package/src/preset.tsx
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-configured Assets tool for FloatingDevTools.
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```tsx
|
|
6
|
-
* import { assetsToolPreset } from '@buoy-gg/assets';
|
|
7
|
-
*
|
|
8
|
-
* <FloatingDevTools apps={[assetsToolPreset]} />
|
|
9
|
-
* ```
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { AssetsIcon, ASSETS_ICON_COLOR } from "./components/AssetsIcon";
|
|
13
|
-
import { AssetsModal } from "./components/AssetsModal";
|
|
14
|
-
|
|
15
|
-
export const assetsToolPreset = {
|
|
16
|
-
id: "assets",
|
|
17
|
-
name: "ASSETS",
|
|
18
|
-
description: "Every bundled asset — sizes, scale coverage, unused detection",
|
|
19
|
-
slot: "both" as const,
|
|
20
|
-
icon: ({ size, color }: { size: number; color?: string }) => (
|
|
21
|
-
<AssetsIcon size={size} color={color || ASSETS_ICON_COLOR} />
|
|
22
|
-
),
|
|
23
|
-
component: AssetsModal,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/** Create a custom Assets tool configuration. */
|
|
27
|
-
export function createAssetsTool(
|
|
28
|
-
options: { name?: string; description?: string } = {}
|
|
29
|
-
) {
|
|
30
|
-
return {
|
|
31
|
-
...assetsToolPreset,
|
|
32
|
-
name: options.name ?? assetsToolPreset.name,
|
|
33
|
-
description: options.description ?? assetsToolPreset.description,
|
|
34
|
-
};
|
|
35
|
-
}
|
package/src/remote.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote-dispatch seam for dashboard mirrors (Buoy Desktop).
|
|
3
|
-
*
|
|
4
|
-
* On device this stays null and every action runs locally. A dashboard sets
|
|
5
|
-
* a handler that forwards action name + params over external-sync to the
|
|
6
|
-
* SAME adapter actions the MCP uses (sync/assetsSyncAdapter.ts), so one
|
|
7
|
-
* action vocabulary serves phone UI, desktop UI, and LLM automation.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { assetsState } from "./state";
|
|
11
|
-
import { invalidate } from "./store/assetStore";
|
|
12
|
-
import type { AssetsSnapshotPayload } from "./types";
|
|
13
|
-
|
|
14
|
-
export type AssetsRemoteDispatch = (
|
|
15
|
-
action: string,
|
|
16
|
-
params?: Record<string, unknown>
|
|
17
|
-
) => Promise<unknown>;
|
|
18
|
-
|
|
19
|
-
export function setRemoteAssetsHandler(
|
|
20
|
-
handler: AssetsRemoteDispatch | null
|
|
21
|
-
): void {
|
|
22
|
-
assetsState().remoteDispatch = handler;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function remoteAssetsDispatch(): AssetsRemoteDispatch | null {
|
|
26
|
-
return assetsState().remoteDispatch;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Dashboards never scan their own bundle — the store is purely a mirror. */
|
|
30
|
-
export function disableAssetsCapture(): void {
|
|
31
|
-
assetsState().captureDisabled = true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Replace the whole store from a synced adapter snapshot. */
|
|
35
|
-
export function replaceAssetsFromSnapshot(payload: AssetsSnapshotPayload): void {
|
|
36
|
-
const s = assetsState();
|
|
37
|
-
const wire = payload.records ?? [];
|
|
38
|
-
s.records = wire.map((w) => ({
|
|
39
|
-
id: w.id,
|
|
40
|
-
key: w.key,
|
|
41
|
-
registryId: w.registryId ?? undefined,
|
|
42
|
-
name: w.name,
|
|
43
|
-
type: w.type,
|
|
44
|
-
kind: w.kind,
|
|
45
|
-
hash: w.hash ?? undefined,
|
|
46
|
-
httpServerLocation: w.location ?? undefined,
|
|
47
|
-
width: w.width ?? undefined,
|
|
48
|
-
height: w.height ?? undefined,
|
|
49
|
-
scales: w.scales,
|
|
50
|
-
resolvedUri: w.uri ?? undefined,
|
|
51
|
-
loaded: w.loaded,
|
|
52
|
-
origin: w.registryId != null ? ("registry" as const) : ("graph" as const),
|
|
53
|
-
sizeBytes: w.sizeBytes ?? undefined,
|
|
54
|
-
sizeSource: w.sizeSource ?? undefined,
|
|
55
|
-
firstSeenAt: Date.now(),
|
|
56
|
-
}));
|
|
57
|
-
s.byKey = new Map(s.records.map((r) => [r.key, r]));
|
|
58
|
-
s.byId = new Map(s.records.map((r) => [r.id, r]));
|
|
59
|
-
if (payload.scanStatus) {
|
|
60
|
-
s.mirrorScanStatus = payload.scanStatus;
|
|
61
|
-
s.fontFamilies = payload.scanStatus.fontFamilies ?? [];
|
|
62
|
-
}
|
|
63
|
-
s.mirrorDiff = payload.diff ?? null;
|
|
64
|
-
invalidate();
|
|
65
|
-
}
|
package/src/state.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global singleton state for the Assets tool.
|
|
3
|
-
*
|
|
4
|
-
* Same rationale as the images tool: the package can be evaluated as two
|
|
5
|
-
* module instances (lib vs src resolution), and the registerAsset patch must
|
|
6
|
-
* share one store with whichever copy the UI renders from. All mutable state
|
|
7
|
-
* lives on globalThis under a well-known symbol.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { AssetRecord, AssetsBaseline, AssetsDiff, AssetsScanStatus } from "./types";
|
|
11
|
-
|
|
12
|
-
export interface AssetsGlobalState {
|
|
13
|
-
// store
|
|
14
|
-
records: AssetRecord[];
|
|
15
|
-
byKey: Map<string, AssetRecord>;
|
|
16
|
-
byId: Map<number, AssetRecord>;
|
|
17
|
-
nextId: number;
|
|
18
|
-
listeners: Set<() => void>;
|
|
19
|
-
snapshot: AssetRecord[];
|
|
20
|
-
emitScheduled: boolean;
|
|
21
|
-
// install / scan status
|
|
22
|
-
installed: boolean;
|
|
23
|
-
patched: boolean;
|
|
24
|
-
registryAvailable: boolean;
|
|
25
|
-
graphSupported: boolean;
|
|
26
|
-
graphCount: number | null;
|
|
27
|
-
graphError: string | null;
|
|
28
|
-
graphFetchInFlight: boolean;
|
|
29
|
-
lastScanAt: number | null;
|
|
30
|
-
measuring: boolean;
|
|
31
|
-
measuredCount: number;
|
|
32
|
-
fontFamilies: string[];
|
|
33
|
-
localAssetCount: number | null;
|
|
34
|
-
// baseline diff
|
|
35
|
-
baseline: AssetsBaseline | null;
|
|
36
|
-
baselineLoaded: boolean;
|
|
37
|
-
// mirror mode (desktop dashboard)
|
|
38
|
-
captureDisabled: boolean;
|
|
39
|
-
remoteDispatch:
|
|
40
|
-
| ((action: string, params?: Record<string, unknown>) => Promise<unknown>)
|
|
41
|
-
| null;
|
|
42
|
-
mirrorScanStatus: AssetsScanStatus | null;
|
|
43
|
-
/** Device PixelRatio adopted from a synced snapshot (scale-gap math). */
|
|
44
|
-
mirrorPixelRatio: number | null;
|
|
45
|
-
/** Device-computed diff adopted from a synced snapshot. */
|
|
46
|
-
mirrorDiff: AssetsDiff | null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const KEY = Symbol.for("buoy.assets.globalState.v1");
|
|
50
|
-
|
|
51
|
-
export function assetsState(): AssetsGlobalState {
|
|
52
|
-
const g = globalThis as Record<symbol, AssetsGlobalState | undefined>;
|
|
53
|
-
if (!g[KEY]) {
|
|
54
|
-
g[KEY] = {
|
|
55
|
-
records: [],
|
|
56
|
-
byKey: new Map(),
|
|
57
|
-
byId: new Map(),
|
|
58
|
-
nextId: 1,
|
|
59
|
-
listeners: new Set(),
|
|
60
|
-
snapshot: [],
|
|
61
|
-
emitScheduled: false,
|
|
62
|
-
installed: false,
|
|
63
|
-
patched: false,
|
|
64
|
-
registryAvailable: false,
|
|
65
|
-
graphSupported: false,
|
|
66
|
-
graphCount: null,
|
|
67
|
-
graphError: null,
|
|
68
|
-
graphFetchInFlight: false,
|
|
69
|
-
lastScanAt: null,
|
|
70
|
-
measuring: false,
|
|
71
|
-
measuredCount: 0,
|
|
72
|
-
fontFamilies: [],
|
|
73
|
-
localAssetCount: null,
|
|
74
|
-
baseline: null,
|
|
75
|
-
baselineLoaded: false,
|
|
76
|
-
captureDisabled: false,
|
|
77
|
-
remoteDispatch: null,
|
|
78
|
-
mirrorScanStatus: null,
|
|
79
|
-
mirrorPixelRatio: null,
|
|
80
|
-
mirrorDiff: null,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
return g[KEY];
|
|
84
|
-
}
|
package/src/store/assetStore.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* assetStore
|
|
3
|
-
*
|
|
4
|
-
* Bounded store of AssetRecords keyed by their stable asset key
|
|
5
|
-
* (`location/name.type`). Follows the images tool's store shape — Set of
|
|
6
|
-
* listeners, frame-coalesced emit, all mutable state on the globalThis
|
|
7
|
-
* singleton (../state.ts) so dual module instances share one store.
|
|
8
|
-
*
|
|
9
|
-
* Unlike the images tool this is an INVENTORY, not an event log: upserts
|
|
10
|
-
* merge into the existing record for a key instead of appending, and there
|
|
11
|
-
* is no ring-buffer cap (a bundle has a bounded asset count by construction).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { assetsState } from "../state";
|
|
15
|
-
import type { AssetKind, AssetRecord, AssetsStats } from "../types";
|
|
16
|
-
|
|
17
|
-
function emit(): void {
|
|
18
|
-
const s = assetsState();
|
|
19
|
-
if (s.emitScheduled) return;
|
|
20
|
-
s.emitScheduled = true;
|
|
21
|
-
requestAnimationFrame(() => {
|
|
22
|
-
s.emitScheduled = false;
|
|
23
|
-
for (const listener of s.listeners) listener();
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Snapshot identity only changes on emit, so useSyncExternalStore stays cheap. */
|
|
28
|
-
export function invalidate(): void {
|
|
29
|
-
const s = assetsState();
|
|
30
|
-
s.snapshot = [...s.records];
|
|
31
|
-
emit();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const KIND_BY_EXT: Record<string, AssetKind> = {
|
|
35
|
-
png: "image", jpg: "image", jpeg: "image", gif: "image", webp: "image",
|
|
36
|
-
bmp: "image", psd: "image", svg: "image", tiff: "image", ktx: "image",
|
|
37
|
-
heic: "image", heif: "image", avif: "image",
|
|
38
|
-
ttf: "font", otf: "font", woff: "font", woff2: "font", ttc: "font",
|
|
39
|
-
mp4: "video", m4v: "video", mov: "video", mpeg: "video", mpg: "video",
|
|
40
|
-
webm: "video", avi: "video",
|
|
41
|
-
mp3: "audio", wav: "audio", aac: "audio", aiff: "audio", caf: "audio",
|
|
42
|
-
m4a: "audio", ogg: "audio",
|
|
43
|
-
json: "data", db: "data", yaml: "data", yml: "data", xml: "data",
|
|
44
|
-
html: "data", pdf: "data", zip: "data",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export function kindForType(type: string): AssetKind {
|
|
48
|
-
return KIND_BY_EXT[type.toLowerCase()] ?? "other";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function assetKey(
|
|
52
|
-
httpServerLocation: string | undefined,
|
|
53
|
-
name: string,
|
|
54
|
-
type: string
|
|
55
|
-
): string {
|
|
56
|
-
// The runtime registry percent-encodes ?unstable_path= locations while the
|
|
57
|
-
// Metro graph endpoint returns them decoded — normalize or every asset
|
|
58
|
-
// splits into a registry record + a graph record.
|
|
59
|
-
let location = httpServerLocation ?? "";
|
|
60
|
-
try {
|
|
61
|
-
location = decodeURIComponent(location);
|
|
62
|
-
} catch {
|
|
63
|
-
// malformed escapes: keep the raw string
|
|
64
|
-
}
|
|
65
|
-
return `${location}/${name}.${type}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface AssetUpsert {
|
|
69
|
-
key: string;
|
|
70
|
-
name: string;
|
|
71
|
-
type: string;
|
|
72
|
-
origin: AssetRecord["origin"];
|
|
73
|
-
registryId?: number;
|
|
74
|
-
hash?: string;
|
|
75
|
-
httpServerLocation?: string;
|
|
76
|
-
width?: number;
|
|
77
|
-
height?: number;
|
|
78
|
-
scales?: number[];
|
|
79
|
-
resolvedUri?: string;
|
|
80
|
-
resolvedScale?: number;
|
|
81
|
-
loaded?: boolean;
|
|
82
|
-
fileSystemLocation?: string;
|
|
83
|
-
files?: string[];
|
|
84
|
-
fileHashes?: string[];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Merge an asset sighting into the store; creates the record on first sight. */
|
|
88
|
-
export function upsertRecord(input: AssetUpsert): AssetRecord {
|
|
89
|
-
const s = assetsState();
|
|
90
|
-
let record = s.byKey.get(input.key);
|
|
91
|
-
if (!record) {
|
|
92
|
-
record = {
|
|
93
|
-
id: s.nextId++,
|
|
94
|
-
key: input.key,
|
|
95
|
-
name: input.name,
|
|
96
|
-
type: input.type,
|
|
97
|
-
kind: kindForType(input.type),
|
|
98
|
-
scales: input.scales ?? [],
|
|
99
|
-
loaded: input.loaded ?? false,
|
|
100
|
-
origin: input.origin,
|
|
101
|
-
firstSeenAt: Date.now(),
|
|
102
|
-
};
|
|
103
|
-
s.records.push(record);
|
|
104
|
-
s.byKey.set(record.key, record);
|
|
105
|
-
s.byId.set(record.id, record);
|
|
106
|
-
}
|
|
107
|
-
if (input.registryId != null) record.registryId = input.registryId;
|
|
108
|
-
if (input.hash != null) record.hash = input.hash;
|
|
109
|
-
if (input.httpServerLocation != null) record.httpServerLocation = input.httpServerLocation;
|
|
110
|
-
if (input.width != null) record.width = input.width;
|
|
111
|
-
if (input.height != null) record.height = input.height;
|
|
112
|
-
if (input.scales && input.scales.length) {
|
|
113
|
-
// Dedupe: foo.png + foo@1x.png both existing yields [1, 1, …] from Metro.
|
|
114
|
-
record.scales = [...new Set(input.scales)].sort((a, b) => a - b);
|
|
115
|
-
}
|
|
116
|
-
if (input.resolvedUri != null) record.resolvedUri = input.resolvedUri;
|
|
117
|
-
if (input.resolvedScale != null) record.resolvedScale = input.resolvedScale;
|
|
118
|
-
if (input.loaded) record.loaded = true;
|
|
119
|
-
if (input.fileSystemLocation != null) record.fileSystemLocation = input.fileSystemLocation;
|
|
120
|
-
if (input.files) record.files = input.files;
|
|
121
|
-
if (input.fileHashes) record.fileHashes = input.fileHashes;
|
|
122
|
-
invalidate();
|
|
123
|
-
return record;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Mutate a record in place (records are owned by the store) and notify. */
|
|
127
|
-
export function updateRecord(
|
|
128
|
-
id: number,
|
|
129
|
-
patch: Partial<AssetRecord>
|
|
130
|
-
): AssetRecord | undefined {
|
|
131
|
-
const s = assetsState();
|
|
132
|
-
const record = s.byId.get(id);
|
|
133
|
-
if (!record) return undefined;
|
|
134
|
-
Object.assign(record, patch);
|
|
135
|
-
invalidate();
|
|
136
|
-
return record;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function getRecord(id: number): AssetRecord | undefined {
|
|
140
|
-
return assetsState().byId.get(id);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function getRecordByKey(key: string): AssetRecord | undefined {
|
|
144
|
-
return assetsState().byKey.get(key);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function getSnapshot(): AssetRecord[] {
|
|
148
|
-
return assetsState().snapshot;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function subscribe(listener: () => void): () => void {
|
|
152
|
-
const s = assetsState();
|
|
153
|
-
s.listeners.add(listener);
|
|
154
|
-
return () => assetsState().listeners.delete(listener);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function clearRecords(): void {
|
|
158
|
-
const s = assetsState();
|
|
159
|
-
s.records = [];
|
|
160
|
-
s.byKey = new Map();
|
|
161
|
-
s.byId = new Map();
|
|
162
|
-
s.measuredCount = 0;
|
|
163
|
-
s.graphCount = null;
|
|
164
|
-
invalidate();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Estimated decoded-bitmap memory (RGBA8888) at the scale this device would
|
|
169
|
-
* pick — the size fallback when bytes can't be measured (release builds).
|
|
170
|
-
*/
|
|
171
|
-
export function estDecodedBytes(record: AssetRecord): number {
|
|
172
|
-
if (record.kind !== "image" || !record.width || !record.height) return 0;
|
|
173
|
-
const scale =
|
|
174
|
-
record.resolvedScale ??
|
|
175
|
-
(record.scales.length ? record.scales[record.scales.length - 1] : 1);
|
|
176
|
-
return Math.round(record.width * scale * record.height * scale * 4);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function computeStats(list: AssetRecord[]): AssetsStats {
|
|
180
|
-
const s = assetsState();
|
|
181
|
-
const byKind: AssetsStats["byKind"] = {
|
|
182
|
-
image: { count: 0, bytes: 0 },
|
|
183
|
-
font: { count: 0, bytes: 0 },
|
|
184
|
-
video: { count: 0, bytes: 0 },
|
|
185
|
-
audio: { count: 0, bytes: 0 },
|
|
186
|
-
data: { count: 0, bytes: 0 },
|
|
187
|
-
other: { count: 0, bytes: 0 },
|
|
188
|
-
};
|
|
189
|
-
const stats: AssetsStats = {
|
|
190
|
-
total: list.length,
|
|
191
|
-
loaded: 0,
|
|
192
|
-
neverLoaded: 0,
|
|
193
|
-
byKind,
|
|
194
|
-
totalBytes: 0,
|
|
195
|
-
measuredCount: 0,
|
|
196
|
-
fontFamilies: s.fontFamilies.length,
|
|
197
|
-
};
|
|
198
|
-
for (const r of list) {
|
|
199
|
-
if (r.loaded) stats.loaded++;
|
|
200
|
-
else stats.neverLoaded++;
|
|
201
|
-
byKind[r.kind].count++;
|
|
202
|
-
if (r.sizeBytes != null && r.sizeSource === "measured") {
|
|
203
|
-
byKind[r.kind].bytes += r.sizeBytes;
|
|
204
|
-
stats.totalBytes += r.sizeBytes;
|
|
205
|
-
stats.measuredCount++;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return stats;
|
|
209
|
-
}
|