rn-iconify 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/IconRenderer.js +54 -12
- package/lib/commonjs/IconRenderer.js.map +1 -1
- package/lib/commonjs/babel/cache-writer.js +102 -26
- package/lib/commonjs/babel/cache-writer.js.map +1 -1
- package/lib/commonjs/babel/plugin.js +129 -44
- package/lib/commonjs/babel/plugin.js.map +1 -1
- package/lib/commonjs/babel/scanner.js +219 -0
- package/lib/commonjs/babel/scanner.js.map +1 -0
- package/lib/commonjs/babel/types.js.map +1 -1
- package/lib/commonjs/cache/CacheManager.js +82 -1
- package/lib/commonjs/cache/CacheManager.js.map +1 -1
- package/lib/commonjs/cache/DiskCache.js +33 -4
- package/lib/commonjs/cache/DiskCache.js.map +1 -1
- package/lib/commonjs/cli/CLAUDE.md +7 -0
- package/lib/commonjs/cli/commands/bundle.js +37 -6
- package/lib/commonjs/cli/commands/bundle.js.map +1 -1
- package/lib/commonjs/config/ConfigManager.js +8 -1
- package/lib/commonjs/config/ConfigManager.js.map +1 -1
- package/lib/commonjs/config/index.js.map +1 -1
- package/lib/commonjs/config/types.js +9 -0
- package/lib/commonjs/config/types.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/metro/devServerMiddleware.js +150 -0
- package/lib/commonjs/metro/devServerMiddleware.js.map +1 -0
- package/lib/commonjs/metro/index.js +20 -0
- package/lib/commonjs/metro/index.js.map +1 -0
- package/lib/commonjs/metro/types.js +6 -0
- package/lib/commonjs/metro/types.js.map +1 -0
- package/lib/commonjs/metro/withRnIconify.js +53 -0
- package/lib/commonjs/metro/withRnIconify.js.map +1 -0
- package/lib/commonjs/network/IconifyAPI.js +30 -2
- package/lib/commonjs/network/IconifyAPI.js.map +1 -1
- package/lib/module/IconRenderer.js +54 -12
- package/lib/module/IconRenderer.js.map +1 -1
- package/lib/module/babel/cache-writer.js +99 -26
- package/lib/module/babel/cache-writer.js.map +1 -1
- package/lib/module/babel/plugin.js +129 -46
- package/lib/module/babel/plugin.js.map +1 -1
- package/lib/module/babel/scanner.js +213 -0
- package/lib/module/babel/scanner.js.map +1 -0
- package/lib/module/babel/types.js.map +1 -1
- package/lib/module/cache/CacheManager.js +82 -1
- package/lib/module/cache/CacheManager.js.map +1 -1
- package/lib/module/cache/DiskCache.js +32 -4
- package/lib/module/cache/DiskCache.js.map +1 -1
- package/lib/module/cli/CLAUDE.md +7 -0
- package/lib/module/cli/commands/bundle.js +37 -6
- package/lib/module/cli/commands/bundle.js.map +1 -1
- package/lib/module/config/ConfigManager.js +8 -1
- package/lib/module/config/ConfigManager.js.map +1 -1
- package/lib/module/config/index.js.map +1 -1
- package/lib/module/config/types.js +9 -0
- package/lib/module/config/types.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/metro/devServerMiddleware.js +143 -0
- package/lib/module/metro/devServerMiddleware.js.map +1 -0
- package/lib/module/metro/index.js +19 -0
- package/lib/module/metro/index.js.map +1 -0
- package/lib/module/metro/types.js +2 -0
- package/lib/module/metro/types.js.map +1 -0
- package/lib/module/metro/withRnIconify.js +48 -0
- package/lib/module/metro/withRnIconify.js.map +1 -0
- package/lib/module/network/IconifyAPI.js +30 -2
- package/lib/module/network/IconifyAPI.js.map +1 -1
- package/lib/typescript/IconRenderer.d.ts.map +1 -1
- package/lib/typescript/babel/cache-writer.d.ts +16 -2
- package/lib/typescript/babel/cache-writer.d.ts.map +1 -1
- package/lib/typescript/babel/plugin.d.ts +5 -0
- package/lib/typescript/babel/plugin.d.ts.map +1 -1
- package/lib/typescript/babel/scanner.d.ts +31 -0
- package/lib/typescript/babel/scanner.d.ts.map +1 -0
- package/lib/typescript/babel/types.d.ts +8 -1
- package/lib/typescript/babel/types.d.ts.map +1 -1
- package/lib/typescript/cache/CacheManager.d.ts +16 -0
- package/lib/typescript/cache/CacheManager.d.ts.map +1 -1
- package/lib/typescript/cache/DiskCache.d.ts +2 -0
- package/lib/typescript/cache/DiskCache.d.ts.map +1 -1
- package/lib/typescript/cli/commands/bundle.d.ts.map +1 -1
- package/lib/typescript/components/Charm.d.ts +1 -1
- package/lib/typescript/components/Dashicons.d.ts +1 -1
- package/lib/typescript/components/Hugeicons.d.ts +1 -1
- package/lib/typescript/components/Ix.d.ts +1 -1
- package/lib/typescript/components/Tabler.d.ts +1 -1
- package/lib/typescript/components/Token.d.ts +1 -1
- package/lib/typescript/components/TokenBranded.d.ts +1 -1
- package/lib/typescript/config/ConfigManager.d.ts +5 -1
- package/lib/typescript/config/ConfigManager.d.ts.map +1 -1
- package/lib/typescript/config/index.d.ts +1 -1
- package/lib/typescript/config/index.d.ts.map +1 -1
- package/lib/typescript/config/types.d.ts +26 -0
- package/lib/typescript/config/types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/metro/devServerMiddleware.d.ts +11 -0
- package/lib/typescript/metro/devServerMiddleware.d.ts.map +1 -0
- package/lib/typescript/metro/index.d.ts +19 -0
- package/lib/typescript/metro/index.d.ts.map +1 -0
- package/lib/typescript/metro/types.d.ts +46 -0
- package/lib/typescript/metro/types.d.ts.map +1 -0
- package/lib/typescript/metro/withRnIconify.d.ts +20 -0
- package/lib/typescript/metro/withRnIconify.d.ts.map +1 -0
- package/lib/typescript/network/IconifyAPI.d.ts.map +1 -1
- package/metro.js +12 -0
- package/package.json +13 -6
- package/src/IconRenderer.tsx +59 -12
- package/src/babel/cache-writer.ts +105 -31
- package/src/babel/plugin.ts +157 -34
- package/src/babel/scanner.ts +274 -0
- package/src/babel/types.ts +9 -1
- package/src/cache/CacheManager.ts +83 -1
- package/src/cache/DiskCache.ts +43 -4
- package/src/cli/CLAUDE.md +7 -0
- package/src/cli/commands/bundle.ts +52 -6
- package/src/config/ConfigManager.ts +9 -0
- package/src/config/index.ts +1 -0
- package/src/config/types.ts +35 -0
- package/src/index.ts +1 -0
- package/src/metro/devServerMiddleware.ts +137 -0
- package/src/metro/index.ts +19 -0
- package/src/metro/types.ts +52 -0
- package/src/metro/withRnIconify.ts +52 -0
- package/src/network/IconifyAPI.ts +23 -1
|
@@ -45,6 +45,11 @@ class CacheManagerImpl {
|
|
|
45
45
|
*/
|
|
46
46
|
private bundledIconsInitialized = false;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Guard to prevent reporting icons during bundle load
|
|
50
|
+
*/
|
|
51
|
+
private isLoadingBundle = false;
|
|
52
|
+
|
|
48
53
|
/**
|
|
49
54
|
* Initialize bundled icons from the Babel plugin cache
|
|
50
55
|
* This should be called automatically on first access
|
|
@@ -73,13 +78,31 @@ class CacheManagerImpl {
|
|
|
73
78
|
return 0;
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
this.isLoadingBundle = true;
|
|
76
82
|
this.bundledIcons = new Map();
|
|
83
|
+
let skippedCount = 0;
|
|
77
84
|
|
|
78
85
|
for (const [iconName, data] of Object.entries(bundle.icons)) {
|
|
79
|
-
|
|
86
|
+
if (data && typeof data.svg === 'string' && data.svg.length > 0) {
|
|
87
|
+
this.bundledIcons.set(iconName, data.svg);
|
|
88
|
+
} else {
|
|
89
|
+
skippedCount++;
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
console.warn(`[rn-iconify] Skipping invalid icon in bundle: ${iconName}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (__DEV__ && skippedCount > 0) {
|
|
97
|
+
console.warn(`[rn-iconify] Skipped ${skippedCount} invalid icons in bundle`);
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
this.bundledIconsInitialized = true;
|
|
101
|
+
|
|
102
|
+
// Warmup memory cache from disk cache
|
|
103
|
+
this.warmup();
|
|
104
|
+
|
|
105
|
+
this.isLoadingBundle = false;
|
|
83
106
|
return this.bundledIcons.size;
|
|
84
107
|
}
|
|
85
108
|
|
|
@@ -142,6 +165,11 @@ class CacheManagerImpl {
|
|
|
142
165
|
set(iconName: string, svg: string): void {
|
|
143
166
|
MemoryCache.set(iconName, svg);
|
|
144
167
|
DiskCache.set(iconName, svg);
|
|
168
|
+
|
|
169
|
+
// Report to Metro dev server for dynamic icon learning
|
|
170
|
+
if (!this.isLoadingBundle) {
|
|
171
|
+
this.reportIconUsage(iconName);
|
|
172
|
+
}
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
/**
|
|
@@ -334,6 +362,60 @@ class CacheManagerImpl {
|
|
|
334
362
|
}
|
|
335
363
|
}
|
|
336
364
|
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Warmup memory cache by loading recent disk cache entries
|
|
368
|
+
* Called automatically at the end of loadBundle()
|
|
369
|
+
*
|
|
370
|
+
* @param maxIcons Maximum number of icons to load into memory
|
|
371
|
+
*/
|
|
372
|
+
warmup(maxIcons: number = 100): void {
|
|
373
|
+
try {
|
|
374
|
+
const keys = DiskCache.keys();
|
|
375
|
+
const toLoad = keys.slice(0, maxIcons);
|
|
376
|
+
|
|
377
|
+
let loaded = 0;
|
|
378
|
+
for (const key of toLoad) {
|
|
379
|
+
if (!MemoryCache.has(key)) {
|
|
380
|
+
const svg = DiskCache.get(key);
|
|
381
|
+
if (svg) {
|
|
382
|
+
MemoryCache.set(key, svg);
|
|
383
|
+
loaded++;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (__DEV__ && loaded > 0) {
|
|
389
|
+
console.log(`[rn-iconify] Warmed up ${loaded} icons from disk cache`);
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Warmup is best-effort, don't fail
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Report icon usage to Metro dev server (DEV only)
|
|
398
|
+
* Fire-and-forget POST to the Metro middleware
|
|
399
|
+
*/
|
|
400
|
+
reportIconUsage(iconName: string): void {
|
|
401
|
+
if (typeof __DEV__ === 'undefined' || !__DEV__) return;
|
|
402
|
+
if (this.isLoadingBundle) return;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// Use global fetch if available (React Native)
|
|
406
|
+
if (typeof fetch === 'function') {
|
|
407
|
+
fetch('http://localhost:8081/__rn_iconify_log', {
|
|
408
|
+
method: 'POST',
|
|
409
|
+
headers: { 'Content-Type': 'application/json' },
|
|
410
|
+
body: JSON.stringify({ icon: iconName }),
|
|
411
|
+
}).catch(() => {
|
|
412
|
+
// Silently ignore - Metro server might not have the plugin
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// Silently ignore
|
|
417
|
+
}
|
|
418
|
+
}
|
|
337
419
|
}
|
|
338
420
|
|
|
339
421
|
// Singleton instance
|
package/src/cache/DiskCache.ts
CHANGED
|
@@ -1,14 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Disk cache using MMKV for persistent icon storage
|
|
3
3
|
* Provides fast synchronous access via JSI
|
|
4
|
+
*
|
|
5
|
+
* Supports both react-native-mmkv v3.x and v4.x
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import
|
|
8
|
+
import * as MMKVModule from 'react-native-mmkv';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* MMKV storage interface (compatible with both v3 and v4)
|
|
12
|
+
*/
|
|
13
|
+
interface MMKVStorage {
|
|
14
|
+
getString(key: string): string | undefined;
|
|
15
|
+
set(key: string, value: string | number | boolean): void;
|
|
16
|
+
getNumber(key: string): number | undefined;
|
|
17
|
+
contains(key: string): boolean;
|
|
18
|
+
delete(key: string): void;
|
|
19
|
+
clearAll(): void;
|
|
20
|
+
getAllKeys(): string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create MMKV instance compatible with both v3.x and v4.x
|
|
25
|
+
*
|
|
26
|
+
* v3.x: import { MMKV } from 'react-native-mmkv' → new MMKV({ id: '...' })
|
|
27
|
+
* v4.x: import { createMMKV } from 'react-native-mmkv' → createMMKV({ id: '...' })
|
|
28
|
+
*/
|
|
29
|
+
function createStorage(id: string): MMKVStorage {
|
|
30
|
+
const config = { id };
|
|
31
|
+
|
|
32
|
+
// v4.x: createMMKV function exists
|
|
33
|
+
if ('createMMKV' in MMKVModule && typeof MMKVModule.createMMKV === 'function') {
|
|
34
|
+
return MMKVModule.createMMKV(config) as MMKVStorage;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// v3.x: MMKV is a constructor
|
|
38
|
+
if ('MMKV' in MMKVModule && typeof MMKVModule.MMKV === 'function') {
|
|
39
|
+
const MMKVClass = MMKVModule.MMKV as new (config: { id: string }) => MMKVStorage;
|
|
40
|
+
return new MMKVClass(config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error(
|
|
44
|
+
'[rn-iconify] Could not initialize MMKV storage. ' +
|
|
45
|
+
'Please ensure react-native-mmkv (v3.x or v4.x) is properly installed.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
7
48
|
|
|
8
49
|
// MMKV instance for icon cache
|
|
9
|
-
const storage =
|
|
10
|
-
id: 'rn-iconify-cache',
|
|
11
|
-
});
|
|
50
|
+
const storage = createStorage('rn-iconify-cache');
|
|
12
51
|
|
|
13
52
|
// Cache metadata storage
|
|
14
53
|
const META_KEY_PREFIX = '__meta:';
|
|
@@ -19,6 +19,52 @@ const ICONIFY_API = 'https://api.iconify.design';
|
|
|
19
19
|
*/
|
|
20
20
|
const FETCH_TIMEOUT = 30000;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Icon data from Iconify API
|
|
24
|
+
*/
|
|
25
|
+
interface IconData {
|
|
26
|
+
body: string;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
left?: number;
|
|
30
|
+
top?: number;
|
|
31
|
+
rotate?: number;
|
|
32
|
+
hFlip?: boolean;
|
|
33
|
+
vFlip?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build SVG string from Iconify icon data with transformation support
|
|
38
|
+
*/
|
|
39
|
+
function buildSvgFromIconData(data: IconData, width: number, height: number): string {
|
|
40
|
+
const left = data.left ?? 0;
|
|
41
|
+
const top = data.top ?? 0;
|
|
42
|
+
const viewBox = `${left} ${top} ${width} ${height}`;
|
|
43
|
+
|
|
44
|
+
// Apply transformations (rotate, hFlip, vFlip)
|
|
45
|
+
let body = data.body;
|
|
46
|
+
const transforms: string[] = [];
|
|
47
|
+
|
|
48
|
+
if (data.rotate) {
|
|
49
|
+
const rotation = data.rotate * 90;
|
|
50
|
+
transforms.push(`rotate(${rotation} ${width / 2} ${height / 2})`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (data.hFlip || data.vFlip) {
|
|
54
|
+
const scaleX = data.hFlip ? -1 : 1;
|
|
55
|
+
const scaleY = data.vFlip ? -1 : 1;
|
|
56
|
+
const translateX = data.hFlip ? width : 0;
|
|
57
|
+
const translateY = data.vFlip ? height : 0;
|
|
58
|
+
transforms.push(`translate(${translateX} ${translateY}) scale(${scaleX} ${scaleY})`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (transforms.length > 0) {
|
|
62
|
+
body = `<g transform="${transforms.join(' ')}">${body}</g>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}">${body}</svg>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
/**
|
|
23
69
|
* Fetch multiple icons with batching
|
|
24
70
|
*/
|
|
@@ -45,10 +91,10 @@ async function fetchIcons(
|
|
|
45
91
|
|
|
46
92
|
// Fetch each prefix batch
|
|
47
93
|
for (const [prefix, names] of byPrefix) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
51
96
|
|
|
97
|
+
try {
|
|
52
98
|
// Sort names alphabetically (Iconify best practice)
|
|
53
99
|
const sortedNames = [...names].sort();
|
|
54
100
|
const url = `${ICONIFY_API}/${prefix}.json?icons=${sortedNames.join(',')}`;
|
|
@@ -58,7 +104,6 @@ async function fetchIcons(
|
|
|
58
104
|
}
|
|
59
105
|
|
|
60
106
|
const response = await fetch(url, { signal: controller.signal });
|
|
61
|
-
clearTimeout(timeout);
|
|
62
107
|
|
|
63
108
|
if (!response.ok) {
|
|
64
109
|
console.error(` Failed to fetch ${prefix}: ${response.status}`);
|
|
@@ -76,8 +121,7 @@ async function fetchIcons(
|
|
|
76
121
|
if (iconData) {
|
|
77
122
|
const width = iconData.width ?? defaultWidth;
|
|
78
123
|
const height = iconData.height ?? defaultHeight;
|
|
79
|
-
const
|
|
80
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${body}</svg>`;
|
|
124
|
+
const svg = buildSvgFromIconData(iconData, width, height);
|
|
81
125
|
|
|
82
126
|
results[`${prefix}:${name}`] = { svg, width, height };
|
|
83
127
|
} else if (verbose) {
|
|
@@ -92,6 +136,8 @@ async function fetchIcons(
|
|
|
92
136
|
console.error(` Error fetching ${prefix}:`, error);
|
|
93
137
|
}
|
|
94
138
|
processed += names.length;
|
|
139
|
+
} finally {
|
|
140
|
+
clearTimeout(timeout);
|
|
95
141
|
}
|
|
96
142
|
}
|
|
97
143
|
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
IconifyAPIConfig,
|
|
9
9
|
CacheConfig,
|
|
10
10
|
PerformanceConfig,
|
|
11
|
+
DefaultsConfig,
|
|
11
12
|
ResolvedConfig,
|
|
12
13
|
} from './types';
|
|
13
14
|
import { DEFAULT_CONFIG } from './types';
|
|
@@ -83,6 +84,13 @@ export const ConfigManager = {
|
|
|
83
84
|
return currentConfig.performance;
|
|
84
85
|
},
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Get defaults configuration
|
|
89
|
+
*/
|
|
90
|
+
getDefaultsConfig(): Required<DefaultsConfig> {
|
|
91
|
+
return currentConfig.defaults;
|
|
92
|
+
},
|
|
93
|
+
|
|
86
94
|
/**
|
|
87
95
|
* Update configuration
|
|
88
96
|
* Merges with existing configuration
|
|
@@ -92,6 +100,7 @@ export const ConfigManager = {
|
|
|
92
100
|
api: deepMerge(currentConfig.api, config.api ?? {}),
|
|
93
101
|
cache: deepMerge(currentConfig.cache, config.cache ?? {}),
|
|
94
102
|
performance: deepMerge(currentConfig.performance, config.performance ?? {}),
|
|
103
|
+
defaults: deepMerge(currentConfig.defaults, config.defaults ?? {}),
|
|
95
104
|
};
|
|
96
105
|
|
|
97
106
|
// Notify listeners
|
package/src/config/index.ts
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -65,6 +65,30 @@ export interface CacheConfig {
|
|
|
65
65
|
diskCachePrefix?: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Default icon rendering configuration
|
|
70
|
+
*/
|
|
71
|
+
export interface DefaultsConfig {
|
|
72
|
+
/**
|
|
73
|
+
* Default placeholder to show while loading icons
|
|
74
|
+
* Set to false to explicitly disable
|
|
75
|
+
* @default undefined (no placeholder - non-breaking)
|
|
76
|
+
*/
|
|
77
|
+
placeholder?: 'skeleton' | 'pulse' | 'shimmer' | false;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enable fade-in transition for non-cached icons
|
|
81
|
+
* @default true
|
|
82
|
+
*/
|
|
83
|
+
fadeIn?: boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fade-in animation duration in milliseconds
|
|
87
|
+
* @default 150
|
|
88
|
+
*/
|
|
89
|
+
fadeInDuration?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
/**
|
|
69
93
|
* Performance monitoring configuration
|
|
70
94
|
*/
|
|
@@ -112,6 +136,11 @@ export interface IconifyConfig {
|
|
|
112
136
|
* Performance monitoring configuration
|
|
113
137
|
*/
|
|
114
138
|
performance?: PerformanceConfig;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Default icon rendering configuration
|
|
142
|
+
*/
|
|
143
|
+
defaults?: DefaultsConfig;
|
|
115
144
|
}
|
|
116
145
|
|
|
117
146
|
/**
|
|
@@ -121,6 +150,7 @@ export interface ResolvedConfig {
|
|
|
121
150
|
api: Required<IconifyAPIConfig>;
|
|
122
151
|
cache: Required<CacheConfig>;
|
|
123
152
|
performance: Required<PerformanceConfig>;
|
|
153
|
+
defaults: Required<DefaultsConfig>;
|
|
124
154
|
}
|
|
125
155
|
|
|
126
156
|
/**
|
|
@@ -146,4 +176,9 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
146
176
|
trackCacheStats: true,
|
|
147
177
|
maxHistorySize: 1000,
|
|
148
178
|
},
|
|
179
|
+
defaults: {
|
|
180
|
+
placeholder: false,
|
|
181
|
+
fadeIn: true,
|
|
182
|
+
fadeInDuration: 150,
|
|
183
|
+
},
|
|
149
184
|
};
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metro Dev Server Middleware
|
|
3
|
+
* Handles runtime icon usage reporting from the app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
9
|
+
import type { UsageFile, RnIconifyMetroOptions } from './types';
|
|
10
|
+
|
|
11
|
+
const USAGE_ENDPOINT = '/__rn_iconify_log';
|
|
12
|
+
const STATUS_ENDPOINT = '/__rn_iconify_status';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the current usage file
|
|
16
|
+
*/
|
|
17
|
+
function readUsageFile(usagePath: string): UsageFile {
|
|
18
|
+
try {
|
|
19
|
+
if (fs.existsSync(usagePath)) {
|
|
20
|
+
const content = fs.readFileSync(usagePath, 'utf-8');
|
|
21
|
+
const data = JSON.parse(content) as UsageFile;
|
|
22
|
+
if (data.version === '1.0.0' && Array.isArray(data.icons)) {
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// File corrupted or doesn't exist
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { version: '1.0.0', icons: [], updatedAt: new Date().toISOString() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Write usage file atomically (temp file + rename)
|
|
35
|
+
*/
|
|
36
|
+
function writeUsageFile(usagePath: string, data: UsageFile): void {
|
|
37
|
+
const dir = path.dirname(usagePath);
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tmpPath = usagePath + '.tmp';
|
|
43
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
44
|
+
fs.renameSync(tmpPath, usagePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse JSON body from request
|
|
49
|
+
*/
|
|
50
|
+
function parseBody(req: IncomingMessage): Promise<Record<string, unknown>> {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
let body = '';
|
|
53
|
+
req.on('data', (chunk: Buffer) => {
|
|
54
|
+
body += chunk.toString();
|
|
55
|
+
});
|
|
56
|
+
req.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
resolve(JSON.parse(body) as Record<string, unknown>);
|
|
59
|
+
} catch {
|
|
60
|
+
reject(new Error('Invalid JSON'));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
req.on('error', reject);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create the dev server middleware handler
|
|
69
|
+
*/
|
|
70
|
+
export function createDevServerMiddleware(options: RnIconifyMetroOptions = {}) {
|
|
71
|
+
const { outputDir = '.rn-iconify', verbose = false } = options;
|
|
72
|
+
const projectRoot = process.cwd();
|
|
73
|
+
const usagePath = path.isAbsolute(outputDir)
|
|
74
|
+
? path.join(outputDir, 'usage.json')
|
|
75
|
+
: path.join(projectRoot, outputDir, 'usage.json');
|
|
76
|
+
|
|
77
|
+
return async function handleRequest(
|
|
78
|
+
req: IncomingMessage,
|
|
79
|
+
res: ServerResponse,
|
|
80
|
+
next: () => void
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const url = req.url;
|
|
83
|
+
|
|
84
|
+
// POST /__rn_iconify_log — report icon usage
|
|
85
|
+
if (req.method === 'POST' && url === USAGE_ENDPOINT) {
|
|
86
|
+
try {
|
|
87
|
+
const body = await parseBody(req);
|
|
88
|
+
const icon = body.icon;
|
|
89
|
+
|
|
90
|
+
if (typeof icon !== 'string' || !icon.includes(':')) {
|
|
91
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
92
|
+
res.end(JSON.stringify({ error: 'Invalid icon name' }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Read existing usage
|
|
97
|
+
const usage = readUsageFile(usagePath);
|
|
98
|
+
|
|
99
|
+
// Deduplicate
|
|
100
|
+
if (!usage.icons.includes(icon)) {
|
|
101
|
+
usage.icons.push(icon);
|
|
102
|
+
usage.updatedAt = new Date().toISOString();
|
|
103
|
+
writeUsageFile(usagePath, usage);
|
|
104
|
+
|
|
105
|
+
if (verbose) {
|
|
106
|
+
console.log(`[rn-iconify:metro] Learned icon: ${icon} (total: ${usage.icons.length})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
111
|
+
res.end(JSON.stringify({ ok: true }));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
114
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// GET /__rn_iconify_status — debug stats
|
|
120
|
+
if (req.method === 'GET' && url === STATUS_ENDPOINT) {
|
|
121
|
+
const usage = readUsageFile(usagePath);
|
|
122
|
+
|
|
123
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
124
|
+
res.end(
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
iconCount: usage.icons.length,
|
|
127
|
+
icons: usage.icons,
|
|
128
|
+
updatedAt: usage.updatedAt,
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Pass through to next middleware
|
|
135
|
+
next();
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rn-iconify Metro Plugin
|
|
3
|
+
*
|
|
4
|
+
* Adds dev server middleware for runtime icon usage learning.
|
|
5
|
+
* When used with the Babel plugin, enables zero-config 0ms rendering.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```js
|
|
9
|
+
* // metro.config.js
|
|
10
|
+
* const { withRnIconify } = require('rn-iconify/metro');
|
|
11
|
+
*
|
|
12
|
+
* const config = getDefaultConfig(__dirname);
|
|
13
|
+
* module.exports = withRnIconify(config);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { withRnIconify } from './withRnIconify';
|
|
18
|
+
export { createDevServerMiddleware } from './devServerMiddleware';
|
|
19
|
+
export type { MetroConfig, RnIconifyMetroOptions, UsageFile } from './types';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metro configuration types
|
|
3
|
+
* Minimal type definitions for Metro config integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Metro middleware function
|
|
10
|
+
*/
|
|
11
|
+
export type MetroMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Metro server configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface MetroServerConfig {
|
|
17
|
+
enhanceMiddleware?: (middleware: MetroMiddleware, server: unknown) => MetroMiddleware;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Metro configuration object (partial, only what we need)
|
|
22
|
+
*/
|
|
23
|
+
export interface MetroConfig {
|
|
24
|
+
server?: MetroServerConfig;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* rn-iconify Metro plugin options
|
|
30
|
+
*/
|
|
31
|
+
export interface RnIconifyMetroOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Directory to store usage data
|
|
34
|
+
* @default '.rn-iconify'
|
|
35
|
+
*/
|
|
36
|
+
outputDir?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enable verbose logging
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
verbose?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Usage file structure
|
|
47
|
+
*/
|
|
48
|
+
export interface UsageFile {
|
|
49
|
+
version: string;
|
|
50
|
+
icons: string[];
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metro Config Wrapper
|
|
3
|
+
* Adds rn-iconify middleware to the Metro dev server
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```js
|
|
7
|
+
* // metro.config.js
|
|
8
|
+
* const { withRnIconify } = require('rn-iconify/metro');
|
|
9
|
+
*
|
|
10
|
+
* const config = getDefaultConfig(__dirname);
|
|
11
|
+
* module.exports = withRnIconify(config);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { MetroConfig, MetroMiddleware, RnIconifyMetroOptions } from './types';
|
|
16
|
+
import { createDevServerMiddleware } from './devServerMiddleware';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap a Metro config to add rn-iconify dev server middleware
|
|
20
|
+
* This enables runtime icon usage learning during development
|
|
21
|
+
*/
|
|
22
|
+
export function withRnIconify(config: MetroConfig, options?: RnIconifyMetroOptions): MetroConfig {
|
|
23
|
+
const middleware = createDevServerMiddleware(options);
|
|
24
|
+
|
|
25
|
+
const existingEnhance = config.server?.enhanceMiddleware;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...config,
|
|
29
|
+
server: {
|
|
30
|
+
...config.server,
|
|
31
|
+
enhanceMiddleware: (metroMiddleware: MetroMiddleware, server: unknown): MetroMiddleware => {
|
|
32
|
+
// Apply existing enhanceMiddleware if present
|
|
33
|
+
const enhanced = existingEnhance
|
|
34
|
+
? existingEnhance(metroMiddleware, server)
|
|
35
|
+
: metroMiddleware;
|
|
36
|
+
|
|
37
|
+
// Wrap with our middleware
|
|
38
|
+
return (req, res, next) => {
|
|
39
|
+
// Check if this is our endpoint
|
|
40
|
+
const url = req.url;
|
|
41
|
+
if (url === '/__rn_iconify_log' || url === '/__rn_iconify_status') {
|
|
42
|
+
middleware(req, res, next);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Otherwise pass through
|
|
47
|
+
enhanced(req, res, next);
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -62,19 +62,41 @@ async function fetchWithTimeout(
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Combine multiple AbortSignals into one
|
|
65
|
+
* Properly cleans up event listeners to prevent memory leaks
|
|
65
66
|
*/
|
|
66
67
|
function anySignal(signals: AbortSignal[]): AbortSignal {
|
|
67
68
|
const controller = new AbortController();
|
|
68
69
|
|
|
70
|
+
// Check if any signal is already aborted
|
|
69
71
|
for (const signal of signals) {
|
|
70
72
|
if (signal.aborted) {
|
|
71
73
|
controller.abort();
|
|
72
74
|
return controller.signal;
|
|
73
75
|
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create abort handlers that we can clean up
|
|
79
|
+
const abortHandlers: Array<{ signal: AbortSignal; handler: () => void }> = [];
|
|
80
|
+
|
|
81
|
+
const cleanup = () => {
|
|
82
|
+
for (const { signal, handler } of abortHandlers) {
|
|
83
|
+
signal.removeEventListener('abort', handler);
|
|
84
|
+
}
|
|
85
|
+
abortHandlers.length = 0;
|
|
86
|
+
};
|
|
74
87
|
|
|
75
|
-
|
|
88
|
+
for (const signal of signals) {
|
|
89
|
+
const handler = () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
controller.abort();
|
|
92
|
+
};
|
|
93
|
+
abortHandlers.push({ signal, handler });
|
|
94
|
+
signal.addEventListener('abort', handler);
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
// Also cleanup when our controller aborts (from timeout)
|
|
98
|
+
controller.signal.addEventListener('abort', cleanup, { once: true });
|
|
99
|
+
|
|
78
100
|
return controller.signal;
|
|
79
101
|
}
|
|
80
102
|
|