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
@@ -1 +1 @@
1
- {"version":3,"file":"IconifyAPI.d.ts","sourceRoot":"","sources":["../../../src/network/IconifyAPI.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAyFH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAcvF;AAsCD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAqGvF;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EAAE,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CA+E3B;AAED;;GAEG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAWvD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,qBAAqB,CAAC,CAoDhC;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,KAAK,GAAE,MAAY,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAgCnB"}
1
+ {"version":3,"file":"IconifyAPI.d.ts","sourceRoot":"","sources":["../../../src/network/IconifyAPI.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA+GH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAcvF;AAsCD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAqGvF;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EAAE,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CA+E3B;AAED;;GAEG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAWvD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,qBAAqB,CAAC,CAoDhC;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,KAAK,GAAE,MAAY,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAgCnB"}
package/metro.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * rn-iconify Metro Plugin
3
+ *
4
+ * Adds dev server middleware for runtime icon usage learning.
5
+ *
6
+ * Usage in metro.config.js:
7
+ * const { withRnIconify } = require('rn-iconify/metro');
8
+ * module.exports = withRnIconify(config);
9
+ */
10
+
11
+ // Re-export the Metro plugin from the built output
12
+ module.exports = require('./lib/commonjs/metro/index.js');
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "rn-iconify",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "268,000+ Iconify icons for React Native with native MMKV caching and full TypeScript autocomplete",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/index.d.ts",
8
8
  "react-native": "src/index.ts",
9
9
  "source": "src/index.ts",
10
+ "sideEffects": false,
10
11
  "exports": {
11
12
  ".": {
12
13
  "types": "./lib/typescript/index.d.ts",
@@ -27,16 +28,19 @@
27
28
  "types": "./lib/typescript/animated/index.d.ts",
28
29
  "import": "./lib/module/animated/index.js",
29
30
  "require": "./lib/commonjs/animated/index.js"
31
+ },
32
+ "./metro": {
33
+ "types": "./lib/typescript/metro/index.d.ts",
34
+ "import": "./lib/module/metro/index.js",
35
+ "require": "./lib/commonjs/metro/index.js"
30
36
  }
31
37
  },
32
- "bin": {
33
- "rn-iconify": "./lib/commonjs/cli/index.js"
34
- },
38
+ "bin": "./lib/commonjs/cli/index.js",
35
39
  "files": [
36
40
  "src",
37
41
  "lib",
38
- "bin",
39
42
  "babel.js",
43
+ "metro.js",
40
44
  "README.md",
41
45
  "assets",
42
46
  "!**/__tests__",
@@ -57,7 +61,7 @@
57
61
  "prepare": "husky",
58
62
  "prepublishOnly": "bob build",
59
63
  "generate-components": "tsx scripts/generate-components.ts",
60
- "release": "npm run build && npm publish"
64
+ "semantic-release": "semantic-release"
61
65
  },
62
66
  "keywords": [
63
67
  "react-native",
@@ -112,6 +116,8 @@
112
116
  "@commitlint/config-conventional": "^19.0.0",
113
117
  "@react-native/babel-preset": "^0.82.1",
114
118
  "@react-native/eslint-config": "^0.74.0",
119
+ "@semantic-release/changelog": "^6.0.3",
120
+ "@semantic-release/git": "^10.0.1",
115
121
  "@testing-library/react-native": "^12.0.0",
116
122
  "@types/jest": "^29.5.0",
117
123
  "@types/react": "^18.2.0",
@@ -129,6 +135,7 @@
129
135
  "react-native-mmkv": "^3.0.0",
130
136
  "react-native-svg": "^15.0.0",
131
137
  "react-test-renderer": "^18.2.0",
138
+ "semantic-release": "^25.0.2",
132
139
  "tsx": "^4.20.6",
133
140
  "typescript": "^5.4.0"
134
141
  },
@@ -10,6 +10,7 @@ import { CacheManager } from './cache/CacheManager';
10
10
  import { fetchIcon } from './network/IconifyAPI';
11
11
  import { PlaceholderFactory } from './placeholder';
12
12
  import { useIconAnimation } from './animated/useIconAnimation';
13
+ import { ConfigManager } from './config';
13
14
  import type { IconRendererProps, IconLoadingState } from './types';
14
15
 
15
16
  /**
@@ -60,13 +61,25 @@ export function IconRenderer({
60
61
  autoPlay = true,
61
62
  onAnimationComplete,
62
63
  }: IconRendererProps) {
64
+ // Resolve defaults from config
65
+ const defaultsConfig = ConfigManager.getDefaultsConfig();
66
+ const effectivePlaceholder = placeholder !== undefined
67
+ ? placeholder
68
+ : (defaultsConfig.placeholder !== false ? defaultsConfig.placeholder : undefined);
69
+ const fadeIn = defaultsConfig.fadeIn;
70
+ const fadeInDuration = defaultsConfig.fadeInDuration;
71
+
63
72
  const [svg, setSvg] = useState<string | null>(null);
64
73
  const [state, setState] = useState<IconLoadingState>('idle');
65
74
  const [showFallback, setShowFallback] = useState(false);
75
+ const [wasCacheHit, setWasCacheHit] = useState(false);
66
76
  const mountedRef = useRef(true);
67
77
  const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
68
78
  const abortControllerRef = useRef<AbortController | null>(null);
69
- const isLoadingRef = useRef(false); // Track loading state for closure
79
+ const isLoadingRef = useRef(false);
80
+
81
+ // Fade-in animation value
82
+ const fadeAnim = useRef(new Animated.Value(0)).current;
70
83
 
71
84
  // Calculate dimensions
72
85
  const iconWidth = propWidth ?? size;
@@ -99,7 +112,9 @@ export function IconRenderer({
99
112
  if (mountedRef.current) {
100
113
  setSvg(cached);
101
114
  setState('loaded');
115
+ setWasCacheHit(true);
102
116
  isLoadingRef.current = false;
117
+ fadeAnim.setValue(1); // No fade for cache hits
103
118
  onLoad?.();
104
119
  }
105
120
  return;
@@ -108,15 +123,15 @@ export function IconRenderer({
108
123
  // 2. Set loading state and start fallback timer
109
124
  setState('loading');
110
125
  isLoadingRef.current = true;
126
+ setWasCacheHit(false);
111
127
 
112
128
  if (fallbackDelay > 0) {
113
129
  fallbackTimerRef.current = setTimeout(() => {
114
- // Use ref instead of state to avoid stale closure
115
130
  if (mountedRef.current && isLoadingRef.current) {
116
131
  setShowFallback(true);
117
132
  }
118
133
  }, fallbackDelay);
119
- } else if (fallback) {
134
+ } else if (fallback || effectivePlaceholder !== undefined) {
120
135
  setShowFallback(true);
121
136
  }
122
137
 
@@ -125,17 +140,28 @@ export function IconRenderer({
125
140
  const fetchedSvg = await fetchIcon(iconName, abortControllerRef.current.signal);
126
141
 
127
142
  if (mountedRef.current) {
128
- // Store in cache
129
143
  CacheManager.set(iconName, fetchedSvg);
130
144
 
131
145
  setSvg(fetchedSvg);
132
146
  setState('loaded');
133
147
  isLoadingRef.current = false;
134
148
  setShowFallback(false);
149
+
150
+ // Fade-in for non-cached icons
151
+ if (fadeIn) {
152
+ fadeAnim.setValue(0);
153
+ Animated.timing(fadeAnim, {
154
+ toValue: 1,
155
+ duration: fadeInDuration,
156
+ useNativeDriver: true,
157
+ }).start();
158
+ } else {
159
+ fadeAnim.setValue(1);
160
+ }
161
+
135
162
  onLoad?.();
136
163
  }
137
164
  } catch (error) {
138
- // Ignore abort errors
139
165
  if (error instanceof Error && error.name === 'AbortError') {
140
166
  return;
141
167
  }
@@ -145,7 +171,7 @@ export function IconRenderer({
145
171
  onError?.(error instanceof Error ? error : new Error(String(error)));
146
172
  }
147
173
  }
148
- }, [iconName, fallback, fallbackDelay, onLoad, onError]); // Removed 'state' from deps
174
+ }, [iconName, fallback, fallbackDelay, onLoad, onError, effectivePlaceholder, fadeIn, fadeInDuration, fadeAnim]);
149
175
 
150
176
  // Effect to load icon
151
177
  useEffect(() => {
@@ -194,6 +220,9 @@ export function IconRenderer({
194
220
  // Determine if we should show placeholder/fallback
195
221
  const shouldShowPlaceholder = (state === 'loading' && showFallback) || state === 'error';
196
222
 
223
+ // Whether to use fade-in wrapper (only for non-cached, non-animated icons)
224
+ const useFadeIn = fadeIn && !wasCacheHit && state === 'loaded' && !hasAnimation;
225
+
197
226
  // Check if icon should be pressable
198
227
  const isPressable = !!(onPress || onLongPress);
199
228
 
@@ -224,8 +253,8 @@ export function IconRenderer({
224
253
 
225
254
  // Render placeholder or fallback during loading/error
226
255
  if (shouldShowPlaceholder) {
227
- // Priority: placeholder > fallback
228
- if (placeholder !== undefined) {
256
+ // Priority: effectivePlaceholder (includes config default) > fallback
257
+ if (effectivePlaceholder !== undefined) {
229
258
  return wrapWithPressable(
230
259
  <View
231
260
  style={[{ width: iconWidth, height: iconHeight }, style]}
@@ -234,7 +263,7 @@ export function IconRenderer({
234
263
  {...nativeWindProps}
235
264
  >
236
265
  <PlaceholderFactory
237
- type={placeholder}
266
+ type={effectivePlaceholder}
238
267
  width={iconWidth}
239
268
  height={iconHeight}
240
269
  color={placeholderColor}
@@ -291,7 +320,25 @@ export function IconRenderer({
291
320
  );
292
321
  }
293
322
 
294
- // Render without animation
323
+ // Render without animation (with optional fade-in)
324
+ if (useFadeIn) {
325
+ return wrapWithPressable(
326
+ <Animated.View
327
+ style={[
328
+ styles.container,
329
+ { width: iconWidth, height: iconHeight, transform: transformStyle, opacity: fadeAnim },
330
+ style,
331
+ ]}
332
+ accessibilityLabel={accessibilityLabel}
333
+ accessibilityRole="image"
334
+ testID={testID}
335
+ {...nativeWindProps}
336
+ >
337
+ <SvgXml xml={colorizedSvg} width={iconWidth} height={iconHeight} />
338
+ </Animated.View>
339
+ );
340
+ }
341
+
295
342
  return wrapWithPressable(
296
343
  <View
297
344
  style={[
@@ -310,7 +357,7 @@ export function IconRenderer({
310
357
  }
311
358
 
312
359
  // Show placeholder immediately if set (no delay), otherwise empty view
313
- if (placeholder !== undefined && state === 'loading') {
360
+ if (effectivePlaceholder !== undefined && state === 'loading') {
314
361
  return wrapWithPressable(
315
362
  <View
316
363
  style={[{ width: iconWidth, height: iconHeight }, style]}
@@ -319,7 +366,7 @@ export function IconRenderer({
319
366
  {...nativeWindProps}
320
367
  >
321
368
  <PlaceholderFactory
322
- type={placeholder}
369
+ type={effectivePlaceholder}
323
370
  width={iconWidth}
324
371
  height={iconHeight}
325
372
  color={placeholderColor}
@@ -233,7 +233,33 @@ export async function fetchAndCreateBundle(
233
233
  }
234
234
 
235
235
  /**
236
- * Write bundle to file
236
+ * Read existing bundle from disk
237
+ * Returns null if file doesn't exist or is invalid
238
+ */
239
+ export function readExistingBundle(bundlePath: string): IconBundle | null {
240
+ try {
241
+ if (!fs.existsSync(bundlePath)) return null;
242
+ const content = fs.readFileSync(bundlePath, 'utf-8');
243
+ const bundle = JSON.parse(content) as IconBundle;
244
+ if (bundle.version === '1.0.0' && typeof bundle.count === 'number' && bundle.icons) {
245
+ return bundle;
246
+ }
247
+ } catch {
248
+ // Invalid bundle file
249
+ }
250
+ return null;
251
+ }
252
+
253
+ /**
254
+ * Determine which icons are new and need to be fetched
255
+ */
256
+ export function getNewIconNames(allIcons: string[], existingBundle: IconBundle | null): string[] {
257
+ if (!existingBundle) return allIcons;
258
+ return allIcons.filter((name) => !existingBundle.icons[name]);
259
+ }
260
+
261
+ /**
262
+ * Write bundle to file (only if content changed)
237
263
  */
238
264
  export function writeBundleToFile(bundle: IconBundle, outputPath: string, verbose: boolean): void {
239
265
  // Ensure directory exists
@@ -242,8 +268,29 @@ export function writeBundleToFile(bundle: IconBundle, outputPath: string, verbos
242
268
  fs.mkdirSync(dir, { recursive: true });
243
269
  }
244
270
 
245
- // Write bundle
271
+ // Write bundle JSON
246
272
  const content = JSON.stringify(bundle);
273
+
274
+ // Check if content actually changed
275
+ if (fs.existsSync(outputPath)) {
276
+ try {
277
+ const existing = fs.readFileSync(outputPath, 'utf-8');
278
+ const existingBundle = JSON.parse(existing) as IconBundle;
279
+ if (existingBundle.count === bundle.count && existingBundle.version === bundle.version) {
280
+ const existingKeys = Object.keys(existingBundle.icons).sort().join(',');
281
+ const newKeys = Object.keys(bundle.icons).sort().join(',');
282
+ if (existingKeys === newKeys) {
283
+ if (verbose) {
284
+ console.log('[rn-iconify] Bundle unchanged, skipping write');
285
+ }
286
+ return;
287
+ }
288
+ }
289
+ } catch {
290
+ // Existing file is corrupted, proceed with write
291
+ }
292
+ }
293
+
247
294
  fs.writeFileSync(outputPath, content, 'utf-8');
248
295
 
249
296
  if (verbose) {
@@ -252,15 +299,26 @@ export function writeBundleToFile(bundle: IconBundle, outputPath: string, verbos
252
299
  }
253
300
  }
254
301
 
302
+ /**
303
+ * Resolve the bundle directory path
304
+ */
305
+ export function resolveBundleDir(outputPath: string, projectRoot: string): string {
306
+ return path.isAbsolute(outputPath)
307
+ ? outputPath
308
+ : path.join(projectRoot, outputPath);
309
+ }
310
+
255
311
  /**
256
312
  * Generate icon bundle from collected icons
313
+ * Supports incremental fetching: only fetches icons not already in the existing bundle
257
314
  */
258
315
  export async function generateBundle(
259
316
  iconNames: string[],
260
317
  options: BabelPluginOptions,
261
- projectRoot: string
318
+ projectRoot: string,
319
+ existingBundle?: IconBundle | null
262
320
  ): Promise<void> {
263
- const { outputPath = 'node_modules/.cache/rn-iconify', verbose = false } = options;
321
+ const { outputPath = '.rn-iconify', verbose = false } = options;
264
322
 
265
323
  if (iconNames.length === 0) {
266
324
  if (verbose) {
@@ -269,28 +327,55 @@ export async function generateBundle(
269
327
  return;
270
328
  }
271
329
 
330
+ const bundleDir = resolveBundleDir(outputPath, projectRoot);
331
+ const bundleFile = path.join(bundleDir, 'icons.json');
332
+
272
333
  try {
273
- // Fetch all icons
274
- const bundle = await fetchAndCreateBundle(iconNames, options);
334
+ // Read existing bundle if not provided
335
+ const existing = existingBundle !== undefined ? existingBundle : readExistingBundle(bundleFile);
336
+
337
+ // Determine which icons need fetching
338
+ const newIconNames = getNewIconNames(iconNames, existing);
275
339
 
276
- // Determine output file path
277
- const bundleFile = path.isAbsolute(outputPath)
278
- ? path.join(outputPath, 'icons.json')
279
- : path.join(projectRoot, outputPath, 'icons.json');
340
+ if (newIconNames.length === 0 && existing) {
341
+ if (verbose) {
342
+ console.log('[rn-iconify] All icons already bundled, skipping fetch');
343
+ }
344
+ return;
345
+ }
346
+
347
+ // Fetch only new icons
348
+ const newBundle = newIconNames.length > 0
349
+ ? await fetchAndCreateBundle(newIconNames, options)
350
+ : { version: '1.0.0', generatedAt: new Date().toISOString(), icons: {}, count: 0 } as IconBundle;
351
+
352
+ // Merge with existing bundle
353
+ const mergedIcons = {
354
+ ...(existing?.icons || {}),
355
+ ...newBundle.icons,
356
+ };
357
+
358
+ const bundle: IconBundle = {
359
+ version: '1.0.0',
360
+ generatedAt: new Date().toISOString(),
361
+ icons: mergedIcons,
362
+ count: Object.keys(mergedIcons).length,
363
+ };
280
364
 
281
365
  // Write bundle to file
282
366
  writeBundleToFile(bundle, bundleFile, verbose);
283
367
 
284
- // Also write a JS module for easy importing
285
- const jsContent = `// Auto-generated by rn-iconify babel plugin
286
- // Do not edit manually
287
- module.exports = ${JSON.stringify(bundle)};
288
- `;
289
- const jsFile = bundleFile.replace('.json', '.js');
368
+ // Also write a JS module for direct require() by Metro
369
+ const jsContent = `// Auto-generated by rn-iconify babel plugin\n// Do not edit manually\nmodule.exports = ${JSON.stringify(bundle)};\n`;
370
+ const jsFile = path.join(bundleDir, 'icons.js');
290
371
  fs.writeFileSync(jsFile, jsContent, 'utf-8');
291
372
 
292
373
  if (verbose) {
293
- console.log(`[rn-iconify] Bundle generation complete!`);
374
+ const newCount = newIconNames.length;
375
+ const existingCount = existing ? Object.keys(existing.icons).length : 0;
376
+ console.log(
377
+ `[rn-iconify] Bundle generation complete! ${newCount} new + ${existingCount} existing = ${bundle.count} total icons`
378
+ );
294
379
  }
295
380
  } catch (error) {
296
381
  // Don't fail the build on bundle generation error
@@ -305,19 +390,8 @@ module.exports = ${JSON.stringify(bundle)};
305
390
  * Check if bundle file exists and is valid
306
391
  */
307
392
  export function isBundleValid(outputPath: string, projectRoot: string): boolean {
308
- const bundleFile = path.isAbsolute(outputPath)
309
- ? path.join(outputPath, 'icons.json')
310
- : path.join(projectRoot, outputPath, 'icons.json');
311
-
312
- if (!fs.existsSync(bundleFile)) {
313
- return false;
314
- }
393
+ const bundleDir = resolveBundleDir(outputPath, projectRoot);
394
+ const bundleFile = path.join(bundleDir, 'icons.json');
315
395
 
316
- try {
317
- const content = fs.readFileSync(bundleFile, 'utf-8');
318
- const bundle = JSON.parse(content) as IconBundle;
319
- return bundle.version === '1.0.0' && typeof bundle.count === 'number';
320
- } catch {
321
- return false;
322
- }
396
+ return readExistingBundle(bundleFile) !== null;
323
397
  }