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,9 +1,16 @@
1
1
  /**
2
2
  * Babel Plugin Visitor
3
3
  * Main visitor logic for the rn-iconify Babel plugin
4
+ *
5
+ * Auto-inject flow:
6
+ * 1. pre hook: scan project → detect icons → check if bundle exists
7
+ * 2. ImportDeclaration visitor: inject loadOfflineBundle + bundle import when bundle exists
8
+ * 3. post hook: generate/update bundle incrementally
4
9
  */
5
10
 
6
- import type { PluginObj, types as BabelTypes } from '@babel/core';
11
+ import * as nodePath from 'path';
12
+ import type { PluginObj, types as BabelTypes, NodePath } from '@babel/core';
13
+ import type { ImportDeclaration } from '@babel/types';
7
14
  import type { BabelPluginState, BabelPluginOptions } from './types';
8
15
  import {
9
16
  COMPONENT_PREFIX_MAP,
@@ -19,7 +26,9 @@ import {
19
26
  getNodeLocation,
20
27
  } from './ast-utils';
21
28
  import { collector } from './collector';
22
- import { generateBundle } from './cache-writer';
29
+ import { generateBundle, readExistingBundle, resolveBundleDir } from './cache-writer';
30
+ import { scanProjectForIcons } from './scanner';
31
+ import type { IconBundle } from './types';
23
32
 
24
33
  /**
25
34
  * Type guard to safely extract plugin options from 'this' context
@@ -63,6 +72,31 @@ let bundleTimer: NodeJS.Timeout | null = null;
63
72
  */
64
73
  let projectRoot = '';
65
74
 
75
+ /**
76
+ * Whether bundle exists at build start (set in pre hook)
77
+ */
78
+ let bundleExists = false;
79
+
80
+ /**
81
+ * Path to the existing bundle directory
82
+ */
83
+ let bundleDirPath = '';
84
+
85
+ /**
86
+ * Whether auto-inject has already been done for this build
87
+ */
88
+ let hasInjected = false;
89
+
90
+ /**
91
+ * Scanned icon names from the pre hook
92
+ */
93
+ let scannedIcons: string[] = [];
94
+
95
+ /**
96
+ * Existing bundle loaded during pre hook
97
+ */
98
+ let existingBundle: IconBundle | null = null;
99
+
66
100
  /**
67
101
  * Create the rn-iconify Babel plugin
68
102
  */
@@ -75,10 +109,8 @@ export function createRnIconifyPlugin(babel: {
75
109
  name: 'rn-iconify',
76
110
 
77
111
  pre(file) {
78
- // Access plugin options through 'this' using type guard approach
79
112
  const opts = getPluginOptions(this);
80
113
 
81
- // Skip if disabled
82
114
  if (opts.disabled) {
83
115
  return;
84
116
  }
@@ -86,12 +118,12 @@ export function createRnIconifyPlugin(babel: {
86
118
  // Initialize collector on first file
87
119
  if (!buildInProgress) {
88
120
  buildInProgress = true;
121
+ hasInjected = false;
89
122
  collector.initialize(opts);
90
123
 
91
- // Detect project root from file - uses type guards for safe access
124
+ // Detect project root from file
92
125
  const filename = getFilenameFromFile(file);
93
126
  if (filename) {
94
- // Try to find project root by looking for package.json
95
127
  let dir = filename;
96
128
  while (dir !== '/') {
97
129
  dir = dir.substring(0, dir.lastIndexOf('/'));
@@ -108,18 +140,41 @@ export function createRnIconifyPlugin(babel: {
108
140
  }
109
141
  }
110
142
 
143
+ const outputPath = opts.outputPath || '.rn-iconify';
144
+ bundleDirPath = resolveBundleDir(outputPath, projectRoot);
145
+ const bundleJsonPath = nodePath.join(bundleDirPath, 'icons.json');
146
+
147
+ // Check if bundle exists
148
+ existingBundle = readExistingBundle(bundleJsonPath);
149
+ bundleExists = existingBundle !== null;
150
+
151
+ // Run sync scanner to discover all icons in the project
152
+ try {
153
+ scannedIcons = scanProjectForIcons(projectRoot, {
154
+ verbose: opts.verbose,
155
+ });
156
+ } catch (error) {
157
+ if (opts.verbose) {
158
+ console.warn('[rn-iconify] Scanner failed:', error);
159
+ }
160
+ scannedIcons = [];
161
+ }
162
+
111
163
  if (opts.verbose) {
112
164
  console.log(`[rn-iconify] Build started. Project root: ${projectRoot}`);
165
+ console.log(`[rn-iconify] Bundle exists: ${bundleExists}`);
166
+ console.log(`[rn-iconify] Scanner found ${scannedIcons.length} icons`);
113
167
  }
114
168
  }
115
169
  },
116
170
 
117
171
  visitor: {
118
172
  /**
119
- * Visit import declarations to track which icon components are imported
120
- * This helps us know which JSX elements to look for
173
+ * Visit import declarations to:
174
+ * 1. Track which icon components are imported
175
+ * 2. Auto-inject loadOfflineBundle when bundle exists
121
176
  */
122
- ImportDeclaration(path, state) {
177
+ ImportDeclaration(path: NodePath<ImportDeclaration>, state: BabelPluginState) {
123
178
  const opts = state.opts || {};
124
179
  if (opts.disabled) return;
125
180
 
@@ -130,13 +185,84 @@ export function createRnIconifyPlugin(babel: {
130
185
  return;
131
186
  }
132
187
 
133
- // Track imported icon components for this file
134
- // (Not strictly necessary since we check COMPONENT_PREFIX_MAP, but could be optimized)
188
+ // Auto-inject: when bundle exists and we haven't injected yet
189
+ const autoInject = opts.autoInject !== false;
190
+ if (autoInject && bundleExists && !hasInjected && source === 'rn-iconify') {
191
+ // Check if this file already has a manual loadOfflineBundle import
192
+ const program = path.findParent((p) => p.isProgram());
193
+ if (program && program.isProgram()) {
194
+ let hasManualLoad = false;
195
+ for (const stmt of program.node.body) {
196
+ if (t.isImportDeclaration(stmt)) {
197
+ for (const spec of stmt.specifiers) {
198
+ if (
199
+ t.isImportSpecifier(spec) &&
200
+ t.isIdentifier(spec.imported) &&
201
+ spec.imported.name === 'loadOfflineBundle'
202
+ ) {
203
+ hasManualLoad = true;
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ if (hasManualLoad) break;
209
+ }
210
+
211
+ if (!hasManualLoad) {
212
+ hasInjected = true;
213
+
214
+ // Compute relative path from this file to the bundle
215
+ const filename = getFilenameFromState(state) || '';
216
+ const bundleJsPath = nodePath.join(bundleDirPath, 'icons.js');
217
+ let relativeBundlePath: string;
218
+
219
+ if (filename) {
220
+ const fileDir = nodePath.dirname(filename);
221
+ relativeBundlePath = nodePath.relative(fileDir, bundleJsPath);
222
+ if (!relativeBundlePath.startsWith('.')) {
223
+ relativeBundlePath = './' + relativeBundlePath;
224
+ }
225
+ } else {
226
+ relativeBundlePath = bundleJsPath;
227
+ }
228
+
229
+ // Import loadOfflineBundle from 'rn-iconify'
230
+ const loadBundleImport = t.importDeclaration(
231
+ [
232
+ t.importSpecifier(
233
+ t.identifier('_rnIconifyLoadBundle'),
234
+ t.identifier('loadOfflineBundle')
235
+ ),
236
+ ],
237
+ t.stringLiteral('rn-iconify')
238
+ );
239
+
240
+ // Import bundle data
241
+ const bundleDataImport = t.importDeclaration(
242
+ [t.importDefaultSpecifier(t.identifier('_rnIconifyBundle'))],
243
+ t.stringLiteral(relativeBundlePath)
244
+ );
245
+
246
+ // Call: _rnIconifyLoadBundle(_rnIconifyBundle)
247
+ const loadCall = t.expressionStatement(
248
+ t.callExpression(t.identifier('_rnIconifyLoadBundle'), [
249
+ t.identifier('_rnIconifyBundle'),
250
+ ])
251
+ );
252
+
253
+ // Insert after the current import
254
+ path.insertAfter([loadBundleImport, bundleDataImport, loadCall]);
255
+
256
+ if (opts.verbose) {
257
+ console.log(`[rn-iconify] Auto-injected bundle loading in ${filename}`);
258
+ }
259
+ }
260
+ }
261
+ }
135
262
  },
136
263
 
137
264
  /**
138
265
  * Visit JSX opening elements to find icon usage
139
- * Handles: <Mdi name="home" />, <Heroicons name="user" />, etc.
140
266
  */
141
267
  JSXOpeningElement(path, state) {
142
268
  const opts = state.opts || {};
@@ -144,17 +270,13 @@ export function createRnIconifyPlugin(babel: {
144
270
 
145
271
  const filename = getFilenameFromState(state) || 'unknown';
146
272
 
147
- // Get component name
148
273
  const componentName = getComponentName(path.node, t);
149
274
  if (!componentName) return;
150
275
 
151
- // Check if it's a valid icon component
152
276
  if (!VALID_COMPONENTS.has(componentName)) return;
153
277
 
154
- // Get the icon name from the 'name' attribute
155
278
  const iconName = getNameAttribute(path.node, t);
156
279
 
157
- // Skip dynamic names
158
280
  if (!iconName) {
159
281
  if (opts.verbose) {
160
282
  const loc = getNodeLocation(path.node);
@@ -165,21 +287,17 @@ export function createRnIconifyPlugin(babel: {
165
287
  return;
166
288
  }
167
289
 
168
- // Get prefix from component name
169
290
  const prefix = COMPONENT_PREFIX_MAP[componentName];
170
291
  if (!prefix) return;
171
292
 
172
- // Build full icon name
173
293
  const fullIconName = `${prefix}:${iconName}`;
174
294
 
175
- // Add to collector
176
295
  const loc = getNodeLocation(path.node);
177
296
  collector.add(fullIconName, filename, loc.line, loc.column);
178
297
  },
179
298
 
180
299
  /**
181
300
  * Visit call expressions to find prefetchIcons usage
182
- * Handles: prefetchIcons(['mdi:home', 'mdi:settings'])
183
301
  */
184
302
  CallExpression(path, state) {
185
303
  const opts = state.opts || {};
@@ -187,14 +305,11 @@ export function createRnIconifyPlugin(babel: {
187
305
 
188
306
  const filename = getFilenameFromState(state) || 'unknown';
189
307
 
190
- // Check if this is a call to prefetchIcons
191
308
  if (!isCallTo(path, 'prefetchIcons', t)) return;
192
309
 
193
- // Get the first argument (should be an array of icon names)
194
310
  const firstArg = path.node.arguments[0];
195
311
  const iconNames = extractArrayStrings(firstArg, t);
196
312
 
197
- // Add each icon to collector
198
313
  const loc = getNodeLocation(path.node);
199
314
  for (const iconName of iconNames) {
200
315
  collector.add(iconName, filename, loc.line, loc.column);
@@ -208,34 +323,37 @@ export function createRnIconifyPlugin(babel: {
208
323
 
209
324
  const filename = getFilenameFromFile(file);
210
325
 
211
- // Mark file as processed
212
326
  if (filename) {
213
327
  processedFiles.add(filename);
214
328
  collector.markFileProcessed(filename);
215
329
  }
216
330
 
217
331
  // Debounce bundle generation
218
- // Metro processes files in parallel, so we wait for a quiet period
219
332
  if (bundleTimer) {
220
333
  clearTimeout(bundleTimer);
221
334
  }
222
335
 
223
336
  bundleTimer = setTimeout(async () => {
224
- // Only generate if we have icons and haven't already generated
225
- if (collector.hasIcons() && !collector.isBundleGenerated()) {
337
+ if (!collector.isBundleGenerated()) {
226
338
  collector.markBundleGenerated();
227
- collector.printSummary();
228
339
 
229
- try {
230
- await generateBundle(collector.getIconNames(), opts, projectRoot);
231
- } catch (error) {
232
- console.error('[rn-iconify] Bundle generation error:', error);
340
+ // Merge AST-collected icons with scanner-collected icons
341
+ const astIcons = collector.getIconNames();
342
+ const allIcons = Array.from(new Set([...astIcons, ...scannedIcons]));
343
+
344
+ if (allIcons.length > 0) {
345
+ collector.printSummary();
346
+
347
+ try {
348
+ await generateBundle(allIcons, opts, projectRoot, existingBundle);
349
+ } catch (error) {
350
+ console.error('[rn-iconify] Bundle generation error:', error);
351
+ }
233
352
  }
234
353
 
235
- // Reset for next build
236
354
  buildInProgress = false;
237
355
  }
238
- }, 500); // Wait 500ms after last file to ensure all files are processed
356
+ }, 500);
239
357
  },
240
358
  };
241
359
  }
@@ -246,6 +364,11 @@ export function createRnIconifyPlugin(babel: {
246
364
  export function resetPluginState(): void {
247
365
  processedFiles.clear();
248
366
  buildInProgress = false;
367
+ hasInjected = false;
368
+ bundleExists = false;
369
+ bundleDirPath = '';
370
+ scannedIcons = [];
371
+ existingBundle = null;
249
372
  if (bundleTimer) {
250
373
  clearTimeout(bundleTimer);
251
374
  bundleTimer = null;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Project File Scanner
3
+ * Synchronously scans project source files for icon usage using regex
4
+ * Used by the Babel plugin to discover all icons before the build
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { COMPONENT_PREFIX_MAP } from './types';
10
+
11
+ /**
12
+ * Scanner options
13
+ */
14
+ export interface ScannerOptions {
15
+ /**
16
+ * File extensions to scan
17
+ * @default ['.tsx', '.jsx', '.ts', '.js']
18
+ */
19
+ extensions?: string[];
20
+
21
+ /**
22
+ * Directories to exclude from scanning
23
+ * @default ['node_modules', 'lib', '.rn-iconify', '__tests__', '__mocks__', 'dist', '.expo', '.git']
24
+ */
25
+ excludeDirs?: string[];
26
+
27
+ /**
28
+ * Enable verbose logging
29
+ * @default false
30
+ */
31
+ verbose?: boolean;
32
+ }
33
+
34
+ const DEFAULT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
35
+ const DEFAULT_EXCLUDE_DIRS = new Set([
36
+ 'node_modules',
37
+ 'lib',
38
+ '.rn-iconify',
39
+ '__tests__',
40
+ '__mocks__',
41
+ 'dist',
42
+ '.expo',
43
+ '.git',
44
+ 'android',
45
+ 'ios',
46
+ 'coverage',
47
+ ]);
48
+
49
+ /**
50
+ * Usage.json file structure (from Metro dev server learning)
51
+ */
52
+ interface UsageFile {
53
+ version: string;
54
+ icons: string[];
55
+ updatedAt: string;
56
+ }
57
+
58
+ /**
59
+ * Build regex patterns for icon component detection
60
+ * Matches: <Ion name="home" /> or <Ion name={'home'} /> or <Ion name={`home`} />
61
+ */
62
+ function buildComponentRegex(): RegExp {
63
+ const componentNames = Object.keys(COMPONENT_PREFIX_MAP).join('|');
64
+ return new RegExp(
65
+ `<(?:${componentNames})\\s[^>]*?name=(?:"([^"]+)"|\\{'([^']+)'\\}|\\{"([^"]+)"\\}|\\\`([^\`]+)\\\`)`,
66
+ 'g'
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Build regex to extract component name from JSX
72
+ */
73
+ function buildComponentNameRegex(): RegExp {
74
+ const componentNames = Object.keys(COMPONENT_PREFIX_MAP).join('|');
75
+ return new RegExp(`<(${componentNames})\\s`, 'g');
76
+ }
77
+
78
+ /**
79
+ * Regex to match prefetchIcons calls
80
+ * Matches: prefetchIcons(['ion:home', 'mdi:settings'])
81
+ */
82
+ const PREFETCH_REGEX = /prefetchIcons\(\s*\[([^\]]*)\]/g;
83
+
84
+ /**
85
+ * Regex to extract string items from array
86
+ */
87
+ const STRING_ITEM_REGEX = /['"]([^'"]+)['"]/g;
88
+
89
+ /**
90
+ * Scan a single file for icon usage
91
+ */
92
+ function scanFile(
93
+ filePath: string,
94
+ componentRegex: RegExp,
95
+ componentNameRegex: RegExp,
96
+ verbose: boolean
97
+ ): string[] {
98
+ const icons: string[] = [];
99
+
100
+ let content: string;
101
+ try {
102
+ content = fs.readFileSync(filePath, 'utf-8');
103
+ } catch {
104
+ return icons;
105
+ }
106
+
107
+ // Skip files that don't import from rn-iconify
108
+ if (!content.includes('rn-iconify') && !content.includes('prefetchIcons')) {
109
+ return icons;
110
+ }
111
+
112
+ // Scan for JSX component usage: <Ion name="home" />
113
+ componentRegex.lastIndex = 0;
114
+ componentNameRegex.lastIndex = 0;
115
+
116
+ // Find all component usages with name attribute
117
+ const fullPattern = new RegExp(componentRegex.source, 'g');
118
+ let match: RegExpExecArray | null;
119
+
120
+ while ((match = fullPattern.exec(content)) !== null) {
121
+ const iconName = match[1] || match[2] || match[3] || match[4];
122
+ if (!iconName) continue;
123
+
124
+ // Find the component name for this match by searching backwards
125
+ const beforeMatch = content.substring(0, match.index + 20);
126
+ const namePattern = new RegExp(componentNameRegex.source, 'g');
127
+ let nameMatch: RegExpExecArray | null;
128
+ let lastNameMatch: RegExpExecArray | null = null;
129
+
130
+ while ((nameMatch = namePattern.exec(beforeMatch)) !== null) {
131
+ if (nameMatch.index <= match.index) {
132
+ lastNameMatch = nameMatch;
133
+ }
134
+ }
135
+
136
+ if (lastNameMatch) {
137
+ const componentName = lastNameMatch[1];
138
+ const prefix = COMPONENT_PREFIX_MAP[componentName];
139
+ if (prefix) {
140
+ icons.push(`${prefix}:${iconName}`);
141
+ if (verbose) {
142
+ console.log(`[rn-iconify:scanner] Found ${prefix}:${iconName} in ${filePath}`);
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // Scan for prefetchIcons calls
149
+ PREFETCH_REGEX.lastIndex = 0;
150
+ while ((match = PREFETCH_REGEX.exec(content)) !== null) {
151
+ const arrayContent = match[1];
152
+ if (!arrayContent) continue;
153
+
154
+ STRING_ITEM_REGEX.lastIndex = 0;
155
+ let itemMatch: RegExpExecArray | null;
156
+ while ((itemMatch = STRING_ITEM_REGEX.exec(arrayContent)) !== null) {
157
+ const iconName = itemMatch[1];
158
+ if (iconName && iconName.includes(':')) {
159
+ icons.push(iconName);
160
+ if (verbose) {
161
+ console.log(`[rn-iconify:scanner] Found prefetch ${iconName} in ${filePath}`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ return icons;
168
+ }
169
+
170
+ /**
171
+ * Recursively walk a directory synchronously
172
+ */
173
+ function walkDirSync(
174
+ dir: string,
175
+ extensions: string[],
176
+ excludeDirs: Set<string>,
177
+ files: string[]
178
+ ): void {
179
+ let entries: fs.Dirent[];
180
+ try {
181
+ entries = fs.readdirSync(dir, { withFileTypes: true });
182
+ } catch {
183
+ return;
184
+ }
185
+
186
+ for (const entry of entries) {
187
+ if (entry.isDirectory()) {
188
+ if (excludeDirs.has(entry.name)) continue;
189
+ walkDirSync(path.join(dir, entry.name), extensions, excludeDirs, files);
190
+ } else if (entry.isFile()) {
191
+ const ext = path.extname(entry.name);
192
+ if (extensions.includes(ext)) {
193
+ files.push(path.join(dir, entry.name));
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Read usage.json from the .rn-iconify directory
201
+ */
202
+ function readUsageFile(projectRoot: string, verbose: boolean): string[] {
203
+ const usagePath = path.join(projectRoot, '.rn-iconify', 'usage.json');
204
+
205
+ try {
206
+ if (!fs.existsSync(usagePath)) return [];
207
+ const content = fs.readFileSync(usagePath, 'utf-8');
208
+ const usage: UsageFile = JSON.parse(content);
209
+
210
+ if (usage.version === '1.0.0' && Array.isArray(usage.icons)) {
211
+ if (verbose) {
212
+ console.log(`[rn-iconify:scanner] Read ${usage.icons.length} icons from usage.json`);
213
+ }
214
+ return usage.icons;
215
+ }
216
+ } catch {
217
+ if (verbose) {
218
+ console.log('[rn-iconify:scanner] Could not read usage.json');
219
+ }
220
+ }
221
+
222
+ return [];
223
+ }
224
+
225
+ /**
226
+ * Scan the entire project for icon usage
227
+ * Returns a deduplicated array of full icon names (e.g., ['ion:home', 'mdi:settings'])
228
+ */
229
+ export function scanProjectForIcons(projectRoot: string, options: ScannerOptions = {}): string[] {
230
+ const { extensions = DEFAULT_EXTENSIONS, excludeDirs, verbose = false } = options;
231
+
232
+ const excludeSet = excludeDirs ? new Set(excludeDirs) : DEFAULT_EXCLUDE_DIRS;
233
+
234
+ const startTime = Date.now();
235
+
236
+ // 1. Walk the project directory
237
+ const files: string[] = [];
238
+ walkDirSync(projectRoot, extensions, excludeSet, files);
239
+
240
+ if (verbose) {
241
+ console.log(`[rn-iconify:scanner] Found ${files.length} source files to scan`);
242
+ }
243
+
244
+ // 2. Build regex patterns once
245
+ const componentRegex = buildComponentRegex();
246
+ const componentNameRegex = buildComponentNameRegex();
247
+
248
+ // 3. Scan each file
249
+ const allIcons: Set<string> = new Set();
250
+
251
+ for (const file of files) {
252
+ const icons = scanFile(file, componentRegex, componentNameRegex, verbose);
253
+ for (const icon of icons) {
254
+ allIcons.add(icon);
255
+ }
256
+ }
257
+
258
+ // 4. Merge with usage.json (dev-learned icons)
259
+ const usageIcons = readUsageFile(projectRoot, verbose);
260
+ for (const icon of usageIcons) {
261
+ allIcons.add(icon);
262
+ }
263
+
264
+ const result = Array.from(allIcons);
265
+ const elapsed = Date.now() - startTime;
266
+
267
+ if (verbose) {
268
+ console.log(
269
+ `[rn-iconify:scanner] Scan complete: ${result.length} unique icons from ${files.length} files in ${elapsed}ms`
270
+ );
271
+ }
272
+
273
+ return result;
274
+ }
@@ -24,7 +24,7 @@ export interface BabelPluginOptions {
24
24
 
25
25
  /**
26
26
  * Output directory for the generated cache
27
- * @default 'node_modules/.cache/rn-iconify'
27
+ * @default '.rn-iconify'
28
28
  */
29
29
  outputPath?: string;
30
30
 
@@ -39,6 +39,14 @@ export interface BabelPluginOptions {
39
39
  * @default false
40
40
  */
41
41
  disabled?: boolean;
42
+
43
+ /**
44
+ * Auto-inject loadOfflineBundle call when bundle exists
45
+ * When true, the plugin will automatically inject import and load
46
+ * statements for the generated icon bundle.
47
+ * @default true
48
+ */
49
+ autoInject?: boolean;
42
50
  }
43
51
 
44
52
  /**