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
package/src/babel/plugin.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
120
|
-
*
|
|
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
|
|
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
|
-
//
|
|
134
|
-
|
|
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
|
-
|
|
225
|
-
if (collector.hasIcons() && !collector.isBundleGenerated()) {
|
|
337
|
+
if (!collector.isBundleGenerated()) {
|
|
226
338
|
collector.markBundleGenerated();
|
|
227
|
-
collector.printSummary();
|
|
228
339
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
|
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
|
+
}
|
package/src/babel/types.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface BabelPluginOptions {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Output directory for the generated cache
|
|
27
|
-
* @default '
|
|
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
|
/**
|