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.
Files changed (122) hide show
  1. package/lib/commonjs/IconRenderer.js +54 -12
  2. package/lib/commonjs/IconRenderer.js.map +1 -1
  3. package/lib/commonjs/babel/cache-writer.js +102 -26
  4. package/lib/commonjs/babel/cache-writer.js.map +1 -1
  5. package/lib/commonjs/babel/plugin.js +129 -44
  6. package/lib/commonjs/babel/plugin.js.map +1 -1
  7. package/lib/commonjs/babel/scanner.js +219 -0
  8. package/lib/commonjs/babel/scanner.js.map +1 -0
  9. package/lib/commonjs/babel/types.js.map +1 -1
  10. package/lib/commonjs/cache/CacheManager.js +82 -1
  11. package/lib/commonjs/cache/CacheManager.js.map +1 -1
  12. package/lib/commonjs/cache/DiskCache.js +33 -4
  13. package/lib/commonjs/cache/DiskCache.js.map +1 -1
  14. package/lib/commonjs/cli/CLAUDE.md +7 -0
  15. package/lib/commonjs/cli/commands/bundle.js +37 -6
  16. package/lib/commonjs/cli/commands/bundle.js.map +1 -1
  17. package/lib/commonjs/config/ConfigManager.js +8 -1
  18. package/lib/commonjs/config/ConfigManager.js.map +1 -1
  19. package/lib/commonjs/config/index.js.map +1 -1
  20. package/lib/commonjs/config/types.js +9 -0
  21. package/lib/commonjs/config/types.js.map +1 -1
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/metro/devServerMiddleware.js +150 -0
  24. package/lib/commonjs/metro/devServerMiddleware.js.map +1 -0
  25. package/lib/commonjs/metro/index.js +20 -0
  26. package/lib/commonjs/metro/index.js.map +1 -0
  27. package/lib/commonjs/metro/types.js +6 -0
  28. package/lib/commonjs/metro/types.js.map +1 -0
  29. package/lib/commonjs/metro/withRnIconify.js +53 -0
  30. package/lib/commonjs/metro/withRnIconify.js.map +1 -0
  31. package/lib/commonjs/network/IconifyAPI.js +30 -2
  32. package/lib/commonjs/network/IconifyAPI.js.map +1 -1
  33. package/lib/module/IconRenderer.js +54 -12
  34. package/lib/module/IconRenderer.js.map +1 -1
  35. package/lib/module/babel/cache-writer.js +99 -26
  36. package/lib/module/babel/cache-writer.js.map +1 -1
  37. package/lib/module/babel/plugin.js +129 -46
  38. package/lib/module/babel/plugin.js.map +1 -1
  39. package/lib/module/babel/scanner.js +213 -0
  40. package/lib/module/babel/scanner.js.map +1 -0
  41. package/lib/module/babel/types.js.map +1 -1
  42. package/lib/module/cache/CacheManager.js +82 -1
  43. package/lib/module/cache/CacheManager.js.map +1 -1
  44. package/lib/module/cache/DiskCache.js +32 -4
  45. package/lib/module/cache/DiskCache.js.map +1 -1
  46. package/lib/module/cli/CLAUDE.md +7 -0
  47. package/lib/module/cli/commands/bundle.js +37 -6
  48. package/lib/module/cli/commands/bundle.js.map +1 -1
  49. package/lib/module/config/ConfigManager.js +8 -1
  50. package/lib/module/config/ConfigManager.js.map +1 -1
  51. package/lib/module/config/index.js.map +1 -1
  52. package/lib/module/config/types.js +9 -0
  53. package/lib/module/config/types.js.map +1 -1
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/metro/devServerMiddleware.js +143 -0
  56. package/lib/module/metro/devServerMiddleware.js.map +1 -0
  57. package/lib/module/metro/index.js +19 -0
  58. package/lib/module/metro/index.js.map +1 -0
  59. package/lib/module/metro/types.js +2 -0
  60. package/lib/module/metro/types.js.map +1 -0
  61. package/lib/module/metro/withRnIconify.js +48 -0
  62. package/lib/module/metro/withRnIconify.js.map +1 -0
  63. package/lib/module/network/IconifyAPI.js +30 -2
  64. package/lib/module/network/IconifyAPI.js.map +1 -1
  65. package/lib/typescript/IconRenderer.d.ts.map +1 -1
  66. package/lib/typescript/babel/cache-writer.d.ts +16 -2
  67. package/lib/typescript/babel/cache-writer.d.ts.map +1 -1
  68. package/lib/typescript/babel/plugin.d.ts +5 -0
  69. package/lib/typescript/babel/plugin.d.ts.map +1 -1
  70. package/lib/typescript/babel/scanner.d.ts +31 -0
  71. package/lib/typescript/babel/scanner.d.ts.map +1 -0
  72. package/lib/typescript/babel/types.d.ts +8 -1
  73. package/lib/typescript/babel/types.d.ts.map +1 -1
  74. package/lib/typescript/cache/CacheManager.d.ts +16 -0
  75. package/lib/typescript/cache/CacheManager.d.ts.map +1 -1
  76. package/lib/typescript/cache/DiskCache.d.ts +2 -0
  77. package/lib/typescript/cache/DiskCache.d.ts.map +1 -1
  78. package/lib/typescript/cli/commands/bundle.d.ts.map +1 -1
  79. package/lib/typescript/components/Charm.d.ts +1 -1
  80. package/lib/typescript/components/Dashicons.d.ts +1 -1
  81. package/lib/typescript/components/Hugeicons.d.ts +1 -1
  82. package/lib/typescript/components/Ix.d.ts +1 -1
  83. package/lib/typescript/components/Tabler.d.ts +1 -1
  84. package/lib/typescript/components/Token.d.ts +1 -1
  85. package/lib/typescript/components/TokenBranded.d.ts +1 -1
  86. package/lib/typescript/config/ConfigManager.d.ts +5 -1
  87. package/lib/typescript/config/ConfigManager.d.ts.map +1 -1
  88. package/lib/typescript/config/index.d.ts +1 -1
  89. package/lib/typescript/config/index.d.ts.map +1 -1
  90. package/lib/typescript/config/types.d.ts +26 -0
  91. package/lib/typescript/config/types.d.ts.map +1 -1
  92. package/lib/typescript/index.d.ts +1 -1
  93. package/lib/typescript/index.d.ts.map +1 -1
  94. package/lib/typescript/metro/devServerMiddleware.d.ts +11 -0
  95. package/lib/typescript/metro/devServerMiddleware.d.ts.map +1 -0
  96. package/lib/typescript/metro/index.d.ts +19 -0
  97. package/lib/typescript/metro/index.d.ts.map +1 -0
  98. package/lib/typescript/metro/types.d.ts +46 -0
  99. package/lib/typescript/metro/types.d.ts.map +1 -0
  100. package/lib/typescript/metro/withRnIconify.d.ts +20 -0
  101. package/lib/typescript/metro/withRnIconify.d.ts.map +1 -0
  102. package/lib/typescript/network/IconifyAPI.d.ts.map +1 -1
  103. package/metro.js +12 -0
  104. package/package.json +13 -6
  105. package/src/IconRenderer.tsx +59 -12
  106. package/src/babel/cache-writer.ts +105 -31
  107. package/src/babel/plugin.ts +157 -34
  108. package/src/babel/scanner.ts +274 -0
  109. package/src/babel/types.ts +9 -1
  110. package/src/cache/CacheManager.ts +83 -1
  111. package/src/cache/DiskCache.ts +43 -4
  112. package/src/cli/CLAUDE.md +7 -0
  113. package/src/cli/commands/bundle.ts +52 -6
  114. package/src/config/ConfigManager.ts +9 -0
  115. package/src/config/index.ts +1 -0
  116. package/src/config/types.ts +35 -0
  117. package/src/index.ts +1 -0
  118. package/src/metro/devServerMiddleware.ts +137 -0
  119. package/src/metro/index.ts +19 -0
  120. package/src/metro/types.ts +52 -0
  121. package/src/metro/withRnIconify.ts +52 -0
  122. 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
- this.bundledIcons.set(iconName, data.svg);
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
@@ -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 { MMKV } from 'react-native-mmkv';
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 = new MMKV({
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:';
@@ -0,0 +1,7 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ _No recent activity_
7
+ </claude-mem-context>
@@ -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
- try {
49
- const controller = new AbortController();
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 body = iconData.body;
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
@@ -22,6 +22,7 @@ export type {
22
22
  IconifyAPIConfig,
23
23
  CacheConfig,
24
24
  PerformanceConfig,
25
+ DefaultsConfig,
25
26
  ResolvedConfig,
26
27
  } from './types';
27
28
 
@@ -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
@@ -150,6 +150,7 @@ export type {
150
150
  IconifyAPIConfig,
151
151
  CacheConfig,
152
152
  PerformanceConfig,
153
+ DefaultsConfig,
153
154
  ResolvedConfig,
154
155
  } from './config';
155
156
 
@@ -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
- signal.addEventListener('abort', () => controller.abort(), { once: true });
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