@buoy-gg/assets 7.0.28 → 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.
Files changed (41) hide show
  1. package/lib/commonjs/components/AssetRow.js +13 -4
  2. package/lib/commonjs/components/AssetRow.js.map +1 -1
  3. package/lib/commonjs/components/AssetsModal.js +2 -1
  4. package/lib/commonjs/components/AssetsModal.js.map +1 -1
  5. package/lib/commonjs/utils/testIDs.js +114 -0
  6. package/lib/commonjs/utils/testIDs.js.map +1 -0
  7. package/lib/module/components/AssetRow.js +13 -4
  8. package/lib/module/components/AssetRow.js.map +1 -1
  9. package/lib/module/components/AssetsModal.js +2 -1
  10. package/lib/module/components/AssetsModal.js.map +1 -1
  11. package/lib/module/utils/testIDs.js +103 -0
  12. package/lib/module/utils/testIDs.js.map +1 -0
  13. package/lib/typescript/components/AssetRow.d.ts.map +1 -1
  14. package/lib/typescript/utils/testIDs.d.ts +75 -0
  15. package/lib/typescript/utils/testIDs.d.ts.map +1 -0
  16. package/package.json +5 -6
  17. package/src/capture/expo.ts +0 -45
  18. package/src/capture/install.ts +0 -73
  19. package/src/capture/measure.ts +0 -167
  20. package/src/capture/metroGraph.ts +0 -146
  21. package/src/capture/registry.ts +0 -137
  22. package/src/components/AssetDetail.tsx +0 -207
  23. package/src/components/AssetRow.tsx +0 -243
  24. package/src/components/AssetsFilterBar.tsx +0 -99
  25. package/src/components/AssetsHeader.tsx +0 -75
  26. package/src/components/AssetsIcon.tsx +0 -9
  27. package/src/components/AssetsListControls.tsx +0 -101
  28. package/src/components/AssetsModal.tsx +0 -370
  29. package/src/components/BaselineBar.tsx +0 -98
  30. package/src/export.ts +0 -110
  31. package/src/format.ts +0 -53
  32. package/src/hooks/useAssetRecords.ts +0 -26
  33. package/src/index.tsx +0 -87
  34. package/src/preset.tsx +0 -35
  35. package/src/remote.ts +0 -65
  36. package/src/state.ts +0 -84
  37. package/src/store/assetStore.ts +0 -209
  38. package/src/store/baseline.ts +0 -152
  39. package/src/store/insights.ts +0 -145
  40. package/src/sync/assetsSyncAdapter.ts +0 -151
  41. package/src/types.ts +0 -160
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Automation handles for the Assets tool.
5
+ *
6
+ * Every handle the tool renders is built here rather than inlined at the call
7
+ * site, for the same reason the highlight tool does it: the ids for rows,
8
+ * badges, filter chips and detail fields are DERIVED from data, and a suite
9
+ * that reimplements the derivation is a suite that can drift from the tool
10
+ * without either side failing. `detox-e2e/assets/fixture.js` mirrors these
11
+ * rules in CommonJS (the tests can't import TypeScript) and asserts the two
12
+ * still agree before any flow trusts a handle.
13
+ *
14
+ * Naming: `buoy-assets-<area>[-<detail>]`, one prefix for the whole tool.
15
+ */
16
+
17
+ /**
18
+ * Lowercase, hyphen-joined, never empty.
19
+ *
20
+ * Shared by rows, badges, filter chips and detail fields so one asset's row
21
+ * handle and its detail handles are slugged the same way — `back-icon.png`
22
+ * becomes `back-icon-png` everywhere it appears.
23
+ */
24
+ export function assetSlug(value) {
25
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
26
+ }
27
+
28
+ /**
29
+ * One inventory row, addressed by filename rather than by store id.
30
+ *
31
+ * The store's numeric ids are assigned in first-sighting order and are NOT
32
+ * stable: clearing the inventory and rescanning renumbers everything, and the
33
+ * order the registry walk and the Metro graph merge in depends on what the app
34
+ * has evaluated so far. A handle built from those makes a test that passes
35
+ * only on the run that recorded it.
36
+ *
37
+ * The tradeoff is that two assets with the same filename in different
38
+ * directories collide. That's a real ambiguity rather than a hidden one —
39
+ * Detox reports it as "multiple elements matched", which names the problem —
40
+ * and it's the same tradeoff the row itself makes, since the row shows the
41
+ * filename and not the path.
42
+ */
43
+ export function assetRowTestID(name, type) {
44
+ return `buoy-assets-row-${assetSlug(`${name}.${type}`)}`;
45
+ }
46
+
47
+ /** A finding badge inside a row: `…-badge-low-res`, `…-badge-dup`. */
48
+ export function assetBadgeTestID(name, type, label) {
49
+ return `${assetRowTestID(name, type)}-badge-${assetSlug(label)}`;
50
+ }
51
+
52
+ /** A kind/status filter chip: `buoy-assets-filter-font`, `…-filter-unused`. */
53
+ export function assetFilterTestID(filter) {
54
+ return `buoy-assets-filter-${assetSlug(filter)}`;
55
+ }
56
+
57
+ /** One label/value pair in the detail view: `buoy-assets-detail-row-hash`. */
58
+ export function assetDetailRowTestID(label) {
59
+ return `buoy-assets-detail-row-${assetSlug(label)}`;
60
+ }
61
+
62
+ /** A loaded font-family chip: `buoy-assets-font-ionicons`. */
63
+ export function assetFontChipTestID(family) {
64
+ return `buoy-assets-font-${assetSlug(family)}`;
65
+ }
66
+
67
+ /** Fixed handles — everything that isn't derived from a record. */
68
+ export const ASSETS_TEST_IDS = {
69
+ // header
70
+ title: "buoy-assets-title",
71
+ copy: "buoy-assets-copy",
72
+ clear: "buoy-assets-clear",
73
+ // list chrome
74
+ list: "buoy-assets-list",
75
+ search: "buoy-assets-search",
76
+ sort: "buoy-assets-sort",
77
+ saveBaseline: "buoy-assets-save-baseline",
78
+ filterBar: "buoy-assets-filter-bar",
79
+ notice: "buoy-assets-notice",
80
+ noticeText: "buoy-assets-notice-text",
81
+ fonts: "buoy-assets-fonts",
82
+ fontsTitle: "buoy-assets-fonts-title",
83
+ empty: "buoy-assets-empty",
84
+ emptyTitle: "buoy-assets-empty-title",
85
+ emptyText: "buoy-assets-empty-text",
86
+ // baseline bar
87
+ baseline: "buoy-assets-baseline",
88
+ baselineSummary: "buoy-assets-baseline-summary",
89
+ baselineUpdate: "buoy-assets-baseline-update",
90
+ baselineClear: "buoy-assets-baseline-clear",
91
+ // detail
92
+ detail: "buoy-assets-detail",
93
+ detailScroll: "buoy-assets-detail-scroll",
94
+ detailPreview: "buoy-assets-detail-preview",
95
+ detailFontPreview: "buoy-assets-detail-font-preview",
96
+ detailFindings: "buoy-assets-detail-findings"
97
+ };
98
+
99
+ /** One finding sentence in the detail view, by position. */
100
+ export function assetFindingTestID(index) {
101
+ return `buoy-assets-detail-finding-${index}`;
102
+ }
103
+ //# sourceMappingURL=testIDs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["assetSlug","value","String","toLowerCase","replace","assetRowTestID","name","type","assetBadgeTestID","label","assetFilterTestID","filter","assetDetailRowTestID","assetFontChipTestID","family","ASSETS_TEST_IDS","title","copy","clear","list","search","sort","saveBaseline","filterBar","notice","noticeText","fonts","fontsTitle","empty","emptyTitle","emptyText","baseline","baselineSummary","baselineUpdate","baselineClear","detail","detailScroll","detailPreview","detailFontPreview","detailFindings","assetFindingTestID","index"],"sourceRoot":"../../../src","sources":["utils/testIDs.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASA,SAASA,CAACC,KAAsB,EAAU;EACxD,OACEC,MAAM,CAACD,KAAK,CAAC,CACVE,WAAW,CAAC,CAAC,CACbC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAC3BA,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,SAAS;AAE3C;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,cAAcA,CAACC,IAAY,EAAEC,IAAY,EAAU;EACjE,OAAO,mBAAmBP,SAAS,CAAC,GAAGM,IAAI,IAAIC,IAAI,EAAE,CAAC,EAAE;AAC1D;;AAEA;AACA,OAAO,SAASC,gBAAgBA,CAC9BF,IAAY,EACZC,IAAY,EACZE,KAAa,EACL;EACR,OAAO,GAAGJ,cAAc,CAACC,IAAI,EAAEC,IAAI,CAAC,UAAUP,SAAS,CAACS,KAAK,CAAC,EAAE;AAClE;;AAEA;AACA,OAAO,SAASC,iBAAiBA,CAACC,MAAc,EAAU;EACxD,OAAO,sBAAsBX,SAAS,CAACW,MAAM,CAAC,EAAE;AAClD;;AAEA;AACA,OAAO,SAASC,oBAAoBA,CAACH,KAAa,EAAU;EAC1D,OAAO,0BAA0BT,SAAS,CAACS,KAAK,CAAC,EAAE;AACrD;;AAEA;AACA,OAAO,SAASI,mBAAmBA,CAACC,MAAc,EAAU;EAC1D,OAAO,oBAAoBd,SAAS,CAACc,MAAM,CAAC,EAAE;AAChD;;AAEA;AACA,OAAO,MAAMC,eAAe,GAAG;EAC7B;EACAC,KAAK,EAAE,mBAAmB;EAC1BC,IAAI,EAAE,kBAAkB;EACxBC,KAAK,EAAE,mBAAmB;EAC1B;EACAC,IAAI,EAAE,kBAAkB;EACxBC,MAAM,EAAE,oBAAoB;EAC5BC,IAAI,EAAE,kBAAkB;EACxBC,YAAY,EAAE,2BAA2B;EACzCC,SAAS,EAAE,wBAAwB;EACnCC,MAAM,EAAE,oBAAoB;EAC5BC,UAAU,EAAE,yBAAyB;EACrCC,KAAK,EAAE,mBAAmB;EAC1BC,UAAU,EAAE,yBAAyB;EACrCC,KAAK,EAAE,mBAAmB;EAC1BC,UAAU,EAAE,yBAAyB;EACrCC,SAAS,EAAE,wBAAwB;EACnC;EACAC,QAAQ,EAAE,sBAAsB;EAChCC,eAAe,EAAE,8BAA8B;EAC/CC,cAAc,EAAE,6BAA6B;EAC7CC,aAAa,EAAE,4BAA4B;EAC3C;EACAC,MAAM,EAAE,oBAAoB;EAC5BC,YAAY,EAAE,2BAA2B;EACzCC,aAAa,EAAE,4BAA4B;EAC3CC,iBAAiB,EAAE,iCAAiC;EACpDC,cAAc,EAAE;AAClB,CAAU;;AAEV;AACA,OAAO,SAASC,kBAAkBA,CAACC,KAAa,EAAU;EACxD,OAAO,8BAA8BA,KAAK,EAAE;AAC9C","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"AssetRow.d.ts","sourceRoot":"","sources":["../../../src/components/AssetRow.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAaD,wBAAgB,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,aAAa,+BAiF/E"}
1
+ {"version":3,"file":"AssetRow.d.ts","sourceRoot":"","sources":["../../../src/components/AssetRow.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAcH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAqBD,wBAAgB,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,aAAa,+BA+F/E"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Automation handles for the Assets tool.
3
+ *
4
+ * Every handle the tool renders is built here rather than inlined at the call
5
+ * site, for the same reason the highlight tool does it: the ids for rows,
6
+ * badges, filter chips and detail fields are DERIVED from data, and a suite
7
+ * that reimplements the derivation is a suite that can drift from the tool
8
+ * without either side failing. `detox-e2e/assets/fixture.js` mirrors these
9
+ * rules in CommonJS (the tests can't import TypeScript) and asserts the two
10
+ * still agree before any flow trusts a handle.
11
+ *
12
+ * Naming: `buoy-assets-<area>[-<detail>]`, one prefix for the whole tool.
13
+ */
14
+ /**
15
+ * Lowercase, hyphen-joined, never empty.
16
+ *
17
+ * Shared by rows, badges, filter chips and detail fields so one asset's row
18
+ * handle and its detail handles are slugged the same way — `back-icon.png`
19
+ * becomes `back-icon-png` everywhere it appears.
20
+ */
21
+ export declare function assetSlug(value: string | number): string;
22
+ /**
23
+ * One inventory row, addressed by filename rather than by store id.
24
+ *
25
+ * The store's numeric ids are assigned in first-sighting order and are NOT
26
+ * stable: clearing the inventory and rescanning renumbers everything, and the
27
+ * order the registry walk and the Metro graph merge in depends on what the app
28
+ * has evaluated so far. A handle built from those makes a test that passes
29
+ * only on the run that recorded it.
30
+ *
31
+ * The tradeoff is that two assets with the same filename in different
32
+ * directories collide. That's a real ambiguity rather than a hidden one —
33
+ * Detox reports it as "multiple elements matched", which names the problem —
34
+ * and it's the same tradeoff the row itself makes, since the row shows the
35
+ * filename and not the path.
36
+ */
37
+ export declare function assetRowTestID(name: string, type: string): string;
38
+ /** A finding badge inside a row: `…-badge-low-res`, `…-badge-dup`. */
39
+ export declare function assetBadgeTestID(name: string, type: string, label: string): string;
40
+ /** A kind/status filter chip: `buoy-assets-filter-font`, `…-filter-unused`. */
41
+ export declare function assetFilterTestID(filter: string): string;
42
+ /** One label/value pair in the detail view: `buoy-assets-detail-row-hash`. */
43
+ export declare function assetDetailRowTestID(label: string): string;
44
+ /** A loaded font-family chip: `buoy-assets-font-ionicons`. */
45
+ export declare function assetFontChipTestID(family: string): string;
46
+ /** Fixed handles — everything that isn't derived from a record. */
47
+ export declare const ASSETS_TEST_IDS: {
48
+ readonly title: "buoy-assets-title";
49
+ readonly copy: "buoy-assets-copy";
50
+ readonly clear: "buoy-assets-clear";
51
+ readonly list: "buoy-assets-list";
52
+ readonly search: "buoy-assets-search";
53
+ readonly sort: "buoy-assets-sort";
54
+ readonly saveBaseline: "buoy-assets-save-baseline";
55
+ readonly filterBar: "buoy-assets-filter-bar";
56
+ readonly notice: "buoy-assets-notice";
57
+ readonly noticeText: "buoy-assets-notice-text";
58
+ readonly fonts: "buoy-assets-fonts";
59
+ readonly fontsTitle: "buoy-assets-fonts-title";
60
+ readonly empty: "buoy-assets-empty";
61
+ readonly emptyTitle: "buoy-assets-empty-title";
62
+ readonly emptyText: "buoy-assets-empty-text";
63
+ readonly baseline: "buoy-assets-baseline";
64
+ readonly baselineSummary: "buoy-assets-baseline-summary";
65
+ readonly baselineUpdate: "buoy-assets-baseline-update";
66
+ readonly baselineClear: "buoy-assets-baseline-clear";
67
+ readonly detail: "buoy-assets-detail";
68
+ readonly detailScroll: "buoy-assets-detail-scroll";
69
+ readonly detailPreview: "buoy-assets-detail-preview";
70
+ readonly detailFontPreview: "buoy-assets-detail-font-preview";
71
+ readonly detailFindings: "buoy-assets-detail-findings";
72
+ };
73
+ /** One finding sentence in the detail view, by position. */
74
+ export declare function assetFindingTestID(index: number): string;
75
+ //# sourceMappingURL=testIDs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testIDs.d.ts","sourceRoot":"","sources":["../../../src/utils/testIDs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAOxD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,MAAM,CAER;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,mEAAmE;AACnE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CA6BlB,CAAC;AAEX,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD"}
package/package.json CHANGED
@@ -1,21 +1,20 @@
1
1
  {
2
2
  "name": "@buoy-gg/assets",
3
- "version": "7.0.28",
3
+ "version": "7.0.30",
4
4
  "description": "See every asset shipped in your React Native app — bundled images, fonts, video and audio with dimensions, scale variants, byte sizes, duplicate detection and WebP-savings audit. Part of Buoy devtools.",
5
- "main": "lib/commonjs/index",
6
- "module": "lib/module/index",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/index.d.ts",
8
- "react-native": "src/index.tsx",
8
+ "react-native": "lib/module/index.js",
9
9
  "source": "src/index.tsx",
10
10
  "files": [
11
- "src",
12
11
  "lib"
13
12
  ],
14
13
  "sideEffects": [
15
14
  "**/index.*"
16
15
  ],
17
16
  "dependencies": {
18
- "@buoy-gg/shared-ui": "^7.0.28"
17
+ "@buoy-gg/shared-ui": "^7.0.30"
19
18
  },
20
19
  "peerDependencies": {
21
20
  "expo-asset": "*",
@@ -1,45 +0,0 @@
1
- /**
2
- * Expo enrichment — all optional, all read through globalThis.expo guards so
3
- * bare RN apps (no expo-modules) silently get none of it and no imports are
4
- * added for anyone.
5
- */
6
-
7
- interface ExpoGlobal {
8
- modules?: {
9
- ExpoFontLoader?: { getLoadedFonts?: () => string[] };
10
- ExpoUpdates?: { localAssets?: Record<string, string> };
11
- };
12
- }
13
-
14
- function expoModules(): ExpoGlobal["modules"] {
15
- return (globalThis as { expo?: ExpoGlobal }).expo?.modules;
16
- }
17
-
18
- /**
19
- * Every loaded font family — build-time embedded (config plugin / UIAppFonts /
20
- * android assets/fonts) AND runtime Font.loadAsync, straight from the native
21
- * module Expo documents as a stable global. [] when unavailable (bare RN).
22
- */
23
- export function getLoadedFontFamilies(): string[] {
24
- try {
25
- const fonts = expoModules()?.ExpoFontLoader?.getLoadedFonts?.();
26
- return Array.isArray(fonts) ? [...fonts].sort() : [];
27
- } catch {
28
- return [];
29
- }
30
- }
31
-
32
- /**
33
- * expo-updates' per-file-hash → local URI map: the COMPLETE shipped asset set
34
- * in Expo Go / OTA environments, independent of what JS has evaluated.
35
- * null when not running in an updates-managed environment.
36
- */
37
- export function getLocalAssets(): Record<string, string> | null {
38
- try {
39
- const map = expoModules()?.ExpoUpdates?.localAssets;
40
- if (map && typeof map === "object" && Object.keys(map).length > 0) return map;
41
- return null;
42
- } catch {
43
- return null;
44
- }
45
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Capture install + scan orchestration.
3
- *
4
- * installAssetCapture() is cheap and synchronous (registry walk + patch) and
5
- * runs at package import. The expensive passes — Metro graph fetch and byte
6
- * measurement — run in rescan(), triggered when the modal opens or via the
7
- * `rescan` sync action, so merely installing Buoy never hits the network.
8
- */
9
-
10
- import { assetsState } from "../state";
11
- import { invalidate } from "../store/assetStore";
12
- import { getLoadedFontFamilies, getLocalAssets } from "./expo";
13
- import { measureSizes } from "./measure";
14
- import { fetchAssetGraph, getDevServerOrigin } from "./metroGraph";
15
- import { getRegistryModule, patchRegisterAsset, walkRegistry } from "./registry";
16
- import type { AssetsScanStatus } from "../types";
17
-
18
- export function installAssetCapture(): void {
19
- const s = assetsState();
20
- if (s.captureDisabled || s.installed) return;
21
- s.installed = true;
22
- s.registryAvailable = getRegistryModule() != null;
23
- s.patched = patchRegisterAsset();
24
- walkRegistry();
25
- refreshEnrichment();
26
- s.lastScanAt = Date.now();
27
- invalidate();
28
- }
29
-
30
- function refreshEnrichment(): void {
31
- const s = assetsState();
32
- s.fontFamilies = getLoadedFontFamilies();
33
- const localAssets = getLocalAssets();
34
- s.localAssetCount = localAssets ? Object.keys(localAssets).length : null;
35
- }
36
-
37
- /**
38
- * Full scan: registry re-walk (catches anything the patch missed), Metro
39
- * graph merge (dev), Expo enrichment refresh, then size measurement.
40
- */
41
- export async function rescan(): Promise<AssetsScanStatus> {
42
- const s = assetsState();
43
- if (s.captureDisabled) return getScanStatus();
44
- installAssetCapture();
45
- walkRegistry();
46
- refreshEnrichment();
47
- await fetchAssetGraph();
48
- s.lastScanAt = Date.now();
49
- invalidate();
50
- void measureSizes();
51
- return getScanStatus();
52
- }
53
-
54
- export function getScanStatus(): AssetsScanStatus {
55
- const s = assetsState();
56
- // Mirrors report the synced DEVICE's status, not the dashboard's.
57
- if (s.mirrorScanStatus) return s.mirrorScanStatus;
58
- let registryCount = 0;
59
- for (const record of s.records) if (record.registryId != null) registryCount++;
60
- return {
61
- registryAvailable: s.registryAvailable,
62
- patched: s.patched,
63
- registryCount,
64
- graphSupported: s.graphSupported || getDevServerOrigin() != null,
65
- graphCount: s.graphCount,
66
- graphError: s.graphError,
67
- lastScanAt: s.lastScanAt,
68
- measuring: s.measuring,
69
- measuredCount: s.measuredCount,
70
- fontFamilies: s.fontFamilies,
71
- localAssetCount: s.localAssetCount,
72
- };
73
- }
@@ -1,167 +0,0 @@
1
- /**
2
- * Byte-size measurement.
3
- *
4
- * Dev only in practice: each scale variant is fetched from the Metro dev
5
- * server (HEAD first for Content-Length, GET-blob fallback since Metro
6
- * doesn't reliably answer HEAD) and the per-variant sizes are summed onto
7
- * the record. Android release resource ids and iOS file:// URIs are not
8
- * measurable from JS — those records keep `estDecodedBytes` as their only
9
- * size signal and the UI marks them as estimates.
10
- */
11
-
12
- import { assetsState } from "../state";
13
- import { getSnapshot, invalidate, updateRecord } from "../store/assetStore";
14
- import { getDevServerOrigin } from "./metroGraph";
15
- import type { AssetRecord } from "../types";
16
-
17
- const CONCURRENCY = 4;
18
-
19
- /**
20
- * Best displayable URL for a record: the resolved runtime URI, or (dev) the
21
- * same dev-server URL the measurement pass uses — this is what gives graph-
22
- * only (never-loaded) images real thumbnails.
23
- */
24
- export function previewURL(record: AssetRecord): string | null {
25
- if (record.resolvedUri) return record.resolvedUri;
26
- const variants = variantURLs(record);
27
- return variants.length ? variants[0]!.url : null;
28
- }
29
-
30
- /**
31
- * Image source for thumbnails/previews. On device the NUMERIC registry id is
32
- * preferred (resolves natively — works for Android release resource ids too).
33
- * On dashboard mirrors that id belongs to the DEVICE's registry and rn-web
34
- * throws "asset with ID N could not be found" — use the device's http(s)
35
- * URL instead (the Metro dev server is reachable from the desktop).
36
- */
37
- export function displaySource(
38
- record: AssetRecord
39
- ): number | { uri: string } | null {
40
- const mirror = assetsState().captureDisabled;
41
- if (!mirror && record.registryId != null) return record.registryId;
42
- const uri = previewURL(record);
43
- if (!uri) return null;
44
- if (mirror && !/^https?:\/\//.test(uri)) return null;
45
- return { uri };
46
- }
47
-
48
- /** All scale-variant URLs for a record, derived without re-encoding pitfalls. */
49
- export function variantURLs(record: AssetRecord): Array<{ scale: number; url: string }> {
50
- const scales = record.scales.length ? record.scales : [1];
51
-
52
- // Preferred: string surgery on the URL resolveAssetSource already proved
53
- // works (handles Expo's ?unstable_path= form and monorepo rewrites).
54
- const resolved = record.resolvedUri;
55
- if (resolved && /^https?:\/\//.test(resolved) && record.resolvedScale != null) {
56
- const pickedSuffix = record.resolvedScale === 1 ? "" : `@${record.resolvedScale}x`;
57
- const marker = `${pickedSuffix}.${record.type}`;
58
- if (resolved.includes(marker) || pickedSuffix === "") {
59
- return scales.map((scale) => {
60
- const suffix = scale === 1 ? "" : `@${scale}x`;
61
- const url = (
62
- pickedSuffix === ""
63
- ? resolved.replace(`.${record.type}`, `${suffix}.${record.type}`)
64
- : resolved.replace(marker, `${suffix}.${record.type}`)
65
- ).replace(/ /g, "%20"); // RN's asset path builder doesn't encode spaces
66
- return { scale, url };
67
- });
68
- }
69
- }
70
-
71
- // Graph-only records: rebuild the dev-server URL the way Metro's
72
- // AssetSourceResolver does (plain concatenation), encoding only spaces —
73
- // full encodeURI would double-encode Expo's pre-escaped unstable_path.
74
- const origin = getDevServerOrigin();
75
- if (!origin || !record.httpServerLocation) return [];
76
- return scales.map((scale) => {
77
- const suffix = scale === 1 ? "" : `@${scale}x`;
78
- const path = `${record.httpServerLocation}/${record.name}${suffix}.${record.type}`;
79
- const sep = path.includes("?") ? "&" : "?";
80
- const url = `${origin}${path}${sep}platform=${platformName()}${
81
- record.hash ? `&hash=${record.hash}` : ""
82
- }`.replace(/ /g, "%20");
83
- return { scale, url };
84
- });
85
- }
86
-
87
- function platformName(): string {
88
- try {
89
- return require("react-native").Platform.OS;
90
- } catch {
91
- return "ios";
92
- }
93
- }
94
-
95
- async function fetchSize(url: string): Promise<number | null> {
96
- try {
97
- const head = await fetch(url, { method: "HEAD" });
98
- if (head.ok) {
99
- const length = Number(head.headers.get("content-length"));
100
- if (Number.isFinite(length) && length > 0) return length;
101
- }
102
- } catch {
103
- // fall through to GET
104
- }
105
- try {
106
- const response = await fetch(url);
107
- if (!response.ok) return null;
108
- const blob = await response.blob();
109
- return blob.size > 0 ? blob.size : null;
110
- } catch {
111
- return null;
112
- }
113
- }
114
-
115
- async function measureRecord(record: AssetRecord): Promise<void> {
116
- const variants = variantURLs(record);
117
- if (!variants.length) {
118
- updateRecord(record.id, { sizeSource: "estimate" });
119
- return;
120
- }
121
- const sizesByScale: Record<string, number> = {};
122
- let total = 0;
123
- for (const { scale, url } of variants) {
124
- const size = await fetchSize(url);
125
- if (size != null) {
126
- sizesByScale[String(scale)] = size;
127
- total += size;
128
- }
129
- }
130
- if (total > 0) {
131
- updateRecord(record.id, { sizesByScale, sizeBytes: total, sizeSource: "measured" });
132
- } else {
133
- updateRecord(record.id, { sizeSource: "estimate" });
134
- }
135
- }
136
-
137
- /** Measure every unmeasured record; safe to re-run (skips measured ones). */
138
- export async function measureSizes(): Promise<{ measured: number; total: number }> {
139
- const s = assetsState();
140
- if (s.measuring) return { measured: s.measuredCount, total: getSnapshot().length };
141
- const pending = getSnapshot().filter((r) => r.sizeSource !== "measured");
142
- s.measuring = true;
143
- invalidate();
144
- try {
145
- let index = 0;
146
- const worker = async () => {
147
- for (;;) {
148
- const next = pending[index++];
149
- if (!next) return;
150
- await measureRecord(next);
151
- s.measuredCount = getSnapshot().filter(
152
- (r) => r.sizeSource === "measured"
153
- ).length;
154
- }
155
- };
156
- await Promise.all(
157
- Array.from({ length: Math.min(CONCURRENCY, pending.length) }, worker)
158
- );
159
- } finally {
160
- s.measuring = false;
161
- invalidate();
162
- }
163
- return {
164
- measured: s.measuredCount,
165
- total: getSnapshot().length,
166
- };
167
- }
@@ -1,146 +0,0 @@
1
- /**
2
- * Metro dev-server graph fetch.
3
- *
4
- * The dev server serves `<entry>.assets` next to `<entry>.bundle` — the full
5
- * dependency graph's AssetData[], INCLUDING assets never required at runtime
6
- * and the `fileSystemLocation`/`files` fields Metro strips from runtime
7
- * records. Deriving the URL by swapping `.bundle` → `.assets` on the running
8
- * bundle's scriptURL keeps the entry path and platform/dev query params
9
- * intact for both RN CLI (index.bundle) and Expo (…/entry.bundle) layouts.
10
- *
11
- * Graph-only records (in the graph, not the registry) are exactly the
12
- * "shipped but never loaded" set. Dev-only: no server, no graph.
13
- */
14
-
15
- import { assetsState } from "../state";
16
- import { assetKey, invalidate, upsertRecord } from "../store/assetStore";
17
-
18
- interface GraphAssetData {
19
- __packager_asset?: boolean;
20
- fileSystemLocation?: string;
21
- httpServerLocation: string;
22
- width?: number | null;
23
- height?: number | null;
24
- scales: number[];
25
- files?: string[];
26
- fileHashes?: string[];
27
- hash: string;
28
- name: string;
29
- type: string;
30
- }
31
-
32
- export function getScriptURL(): string | null {
33
- try {
34
- const { NativeModules } = require("react-native");
35
- const sourceCode = NativeModules?.SourceCode;
36
- const url: unknown =
37
- sourceCode?.getConstants?.().scriptURL ?? sourceCode?.scriptURL;
38
- return typeof url === "string" ? url : null;
39
- } catch {
40
- return null;
41
- }
42
- }
43
-
44
- /** http(s) origin of the Metro dev server, or null when running embedded. */
45
- export function getDevServerOrigin(): string | null {
46
- const scriptURL = getScriptURL();
47
- const match = scriptURL?.match(/^https?:\/\/[^/]+/);
48
- return match ? match[0] : null;
49
- }
50
-
51
- /**
52
- * Candidate graph URLs, most-likely first. The straight `.bundle`→`.assets`
53
- * swap works for RN CLI, but Expo dev builds serve a VIRTUAL entry
54
- * (.expo/.virtual-metro-entry.bundle) that only exists for .bundle requests —
55
- * its .assets twin 404s — so the real entry modules are tried next, keeping
56
- * the original query string (platform/dev/etc.) intact.
57
- */
58
- export function getGraphURLCandidates(): string[] {
59
- const scriptURL = getScriptURL();
60
- if (!scriptURL || !/^https?:\/\//.test(scriptURL)) return [];
61
- const origin = getDevServerOrigin();
62
- const queryIndex = scriptURL.indexOf("?");
63
- const query = queryIndex >= 0 ? scriptURL.slice(queryIndex) : "?platform=ios&dev=true";
64
- const candidates: string[] = [];
65
- if (scriptURL.includes(".bundle")) {
66
- candidates.push(scriptURL.replace(".bundle", ".assets"));
67
- }
68
- if (origin) {
69
- candidates.push(
70
- `${origin}/node_modules/expo-router/entry.assets${query}`,
71
- `${origin}/node_modules/expo/AppEntry.assets${query}`,
72
- `${origin}/index.assets${query}`
73
- );
74
- }
75
- return candidates;
76
- }
77
-
78
- /** Fetch the full asset graph from the dev server and merge it into the store. */
79
- export async function fetchAssetGraph(): Promise<
80
- { ok: true; count: number } | { ok: false; message: string }
81
- > {
82
- const s = assetsState();
83
- const candidates = getGraphURLCandidates();
84
- if (!candidates.length) {
85
- s.graphSupported = false;
86
- s.graphError = "No Metro dev server (embedded bundle)";
87
- invalidate();
88
- return { ok: false, message: s.graphError };
89
- }
90
- if (s.graphFetchInFlight) return { ok: false, message: "Graph fetch already running" };
91
- s.graphFetchInFlight = true;
92
- try {
93
- let data: unknown = null;
94
- let lastError = "";
95
- for (const url of candidates) {
96
- try {
97
- const response = await fetch(url);
98
- if (!response.ok) {
99
- lastError = `Metro responded ${response.status} for ${url}`;
100
- continue;
101
- }
102
- const parsed: unknown = await response.json();
103
- if (Array.isArray(parsed)) {
104
- data = parsed;
105
- break;
106
- }
107
- lastError = "Unexpected .assets payload shape";
108
- } catch (error) {
109
- lastError = String((error as Error)?.message ?? error);
110
- }
111
- }
112
- if (!Array.isArray(data)) throw new Error(lastError || "No graph endpoint answered");
113
- const graph = data as GraphAssetData[];
114
- for (const asset of graph) {
115
- if (!asset || typeof asset.name !== "string" || typeof asset.type !== "string") {
116
- continue;
117
- }
118
- upsertRecord({
119
- key: assetKey(asset.httpServerLocation, asset.name, asset.type),
120
- name: asset.name,
121
- type: asset.type,
122
- origin: "graph",
123
- hash: asset.hash,
124
- httpServerLocation: asset.httpServerLocation,
125
- width: asset.width ?? undefined,
126
- height: asset.height ?? undefined,
127
- scales: asset.scales,
128
- fileSystemLocation: asset.fileSystemLocation,
129
- files: asset.files,
130
- fileHashes: asset.fileHashes,
131
- });
132
- }
133
- s.graphSupported = true;
134
- s.graphCount = graph.length;
135
- s.graphError = null;
136
- invalidate();
137
- return { ok: true, count: graph.length };
138
- } catch (error) {
139
- s.graphSupported = getDevServerOrigin() != null;
140
- s.graphError = String((error as Error)?.message ?? error);
141
- invalidate();
142
- return { ok: false, message: s.graphError };
143
- } finally {
144
- s.graphFetchInFlight = false;
145
- }
146
- }