@@ -11,6 +11,22 @@ import { loadConfig } from "../dist/loadConfig.js";
1111const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
1212const ICONS = new Set ( ) ;
1313
14+ // Props that may behave differently in prod (sprite mode) vs dev
15+ const RISKY_PROPS = new Set ( [
16+ // Color props - should use className="text-*" instead
17+ "stroke" , "fill" , "color" ,
18+ // Stroke presentation attributes - don't cascade into <use> shadow DOM
19+ "strokeLinecap" , "strokeLinejoin" , "strokeDasharray" ,
20+ "strokeDashoffset" , "strokeMiterlimit" , "strokeOpacity" ,
21+ // Fill presentation attributes
22+ "fillOpacity" , "fillRule" ,
23+ // Other presentation attributes
24+ "opacity" , "transform" , "vectorEffect" ,
25+ ] ) ;
26+
27+ // Track warnings: { file, line, icon, prop }
28+ const riskyPropWarnings = [ ] ;
29+
1430// Load user config (merged with defaults)
1531const { IMPORT_NAME , ROOT_DIR , IGNORE_ICONS , EXCLUDE_DIRS } = await loadConfig ( ) ;
1632
@@ -53,49 +69,69 @@ function collect(dir) {
5369 } ) ;
5470 if ( ! ast ) continue ;
5571
56- // Track local "Icon" aliases in this file
72+ // Track local icon names imported from our library
5773 const iconLocalNames = new Set ( ) ;
74+ const iconImports = new Map ( ) ; // local name -> imported name
5875
5976 traverseImport . default ( ast , {
60- ImportDeclaration ( path ) {
61- if ( path . node . source . value !== IMPORT_NAME ) return ;
62- for ( const spec of path . node . specifiers ) {
77+ ImportDeclaration ( nodePath ) {
78+ if ( nodePath . node . source . value !== IMPORT_NAME ) return ;
79+ for ( const spec of nodePath . node . specifiers ) {
6380 if ( t . isImportSpecifier ( spec ) ) {
6481 const imported = spec . imported . name ;
6582 const local = spec . local . name ;
6683 if ( imported === "Icon" ) {
67- // generic Component: track for JSX
6884 iconLocalNames . add ( local ) ;
69- // ignore CustomIcon imports
7085 } else if ( ! IGNORE_ICONS . includes ( imported ) ) {
71- // named icon import
7286 ICONS . add ( imported ) ;
87+ iconLocalNames . add ( local ) ;
88+ iconImports . set ( local , imported ) ;
7389 }
7490 }
7591 }
7692 } ,
77- JSXElement ( path ) {
78- const opening = path . get ( "openingElement" ) ;
93+ JSXElement ( nodePath ) {
94+ const opening = nodePath . get ( "openingElement" ) ;
7995 const nameNode = opening . get ( "name" ) ;
8096 if ( ! nameNode . isJSXIdentifier ( ) ) return ;
8197 const tag = nameNode . node . name ;
98+
99+ // Check if this is one of our icon components
82100 if ( ! iconLocalNames . has ( tag ) ) return ;
83- // found <Icon ...>
101+
102+ const iconName = iconImports . get ( tag ) || tag ;
103+ const line = nodePath . node . loc ?. start ?. line || "?" ;
104+
105+ // Check for risky props
84106 for ( const attrPath of opening . get ( "attributes" ) ) {
85107 if ( ! attrPath . isJSXAttribute ( ) ) continue ;
86- const attrName = attrPath . get ( "name" ) ;
87- if ( ! attrName . isJSXIdentifier ( { name : "name" } ) ) continue ;
88- const valuePath = attrPath . get ( "value" ) ;
89- if ( valuePath . isStringLiteral ( ) ) {
90- ICONS . add ( valuePath . node . value ) ;
91- } else if ( valuePath . isJSXExpressionContainer ( ) ) {
92- const exprPath = valuePath . get ( "expression" ) ;
93- if ( exprPath . evaluate ) {
94- const res = exprPath . evaluate ( ) ;
95- if ( res . confident && typeof res . value === "string" ) {
96- ICONS . add ( res . value ) ;
97- } else {
98- throw path . buildCodeFrameError ( `Unable to statically evaluate <Icon name={...}> at ${ full } ` ) ;
108+ const attrNameNode = attrPath . get ( "name" ) ;
109+ if ( ! attrNameNode . isJSXIdentifier ( ) ) continue ;
110+ const propName = attrNameNode . node . name ;
111+
112+ if ( RISKY_PROPS . has ( propName ) ) {
113+ riskyPropWarnings . push ( {
114+ file : path . relative ( projectRoot , full ) ,
115+ line,
116+ icon : iconName ,
117+ prop : propName ,
118+ } ) ;
119+ }
120+
121+ // Handle <Icon name="..."> for generic Icon component
122+ if ( propName === "name" ) {
123+ const valuePath = attrPath . get ( "value" ) ;
124+ if ( valuePath . isStringLiteral ( ) ) {
125+ ICONS . add ( valuePath . node . value ) ;
126+ } else if ( valuePath . isJSXExpressionContainer ( ) ) {
127+ const exprPath = valuePath . get ( "expression" ) ;
128+ if ( exprPath . evaluate ) {
129+ const res = exprPath . evaluate ( ) ;
130+ if ( res . confident && typeof res . value === "string" ) {
131+ ICONS . add ( res . value ) ;
132+ } else {
133+ throw nodePath . buildCodeFrameError ( `Unable to statically evaluate <Icon name={...}> at ${ full } ` ) ;
134+ }
99135 }
100136 }
101137 }
@@ -111,3 +147,31 @@ collect(scanRoot);
111147const outFile = path . join ( __dirname , "used-icons.js" ) ;
112148fs . writeFileSync ( outFile , `export const ICONS = ${ JSON . stringify ( [ ...ICONS ] . sort ( ) , null , 2 ) } ;\n` , "utf8" ) ;
113149console . log ( `✅ Found ${ ICONS . size } icons; wrote to ${ outFile } ` ) ;
150+
151+ // 4️⃣ Warn about risky props
152+ if ( riskyPropWarnings . length > 0 ) {
153+ console . log ( "" ) ;
154+ console . warn ( `⚠️ Found ${ riskyPropWarnings . length } icon(s) with props that may differ in prod:` ) ;
155+
156+ // Group by file for cleaner output
157+ const byFile = new Map ( ) ;
158+ for ( const w of riskyPropWarnings ) {
159+ if ( ! byFile . has ( w . file ) ) byFile . set ( w . file , [ ] ) ;
160+ byFile . get ( w . file ) . push ( w ) ;
161+ }
162+
163+ for ( const [ file , warnings ] of byFile ) {
164+ console . warn ( ` ${ file } :` ) ;
165+ for ( const w of warnings . slice ( 0 , 5 ) ) {
166+ console . warn ( ` Line ${ w . line } : <${ w . icon } ${ w . prop } ="..." /> - "${ w . prop } " may not work in prod` ) ;
167+ }
168+ if ( warnings . length > 5 ) {
169+ console . warn ( ` ... and ${ warnings . length - 5 } more` ) ;
170+ }
171+ }
172+
173+ console . warn ( "" ) ;
174+ console . warn ( ` 💡 Tip: Use className="text-red-500" for colors (works via currentColor)` ) ;
175+ console . warn ( ` 💡 Props like strokeLinecap, fill, stroke don't cascade into <use> shadow DOM` ) ;
176+ console . warn ( "" ) ;
177+ }
0 commit comments