11import generate from "@babel/generator"
2- import { default as traverse } from "@babel/traverse"
32import { parse } from "@babel/parser"
3+ import { default as traverse } from "@babel/traverse"
44
5- import * as t from "@babel/types"
6- import type { OutputAsset , Plugin } from "rollup"
7- import postcss from "postcss"
85import { existsSync } from "node:fs"
96import fs from "node:fs/promises"
7+ import * as t from "@babel/types"
8+ import postcss from "postcss"
9+ import type { OutputAsset , Plugin } from "rollup"
1010
1111let classPrefix = ""
1212let classesToBePrefix : string [ ] = [ ]
@@ -18,6 +18,9 @@ let classesToPrefixRegexJS: RegExp
1818//CSS:
1919let classesToPrefixRegexCSS : RegExp
2020
21+ const bgenerate = generate . default
22+ const btraverse = traverse . default
23+
2124// the css prefixing is done by postcss
2225async function prefixCSS ( code : string , fileName : string ) {
2326 const pcssPrefixerPlugin : postcss . Plugin = {
@@ -36,72 +39,109 @@ async function prefixCSS(code: string, fileName: string) {
3639 return result
3740}
3841
39- /**
40- * Uses a regex to prefix the classes inside className or class strings
41- */
42- async function prefixJSX ( code : string , fileName : string ) {
43- let replacements = 0
44-
45- const prefixFunc = ( stringVal : string , shouldCount = true ) => {
46- let _replacements = 0
47- const replaced = stringVal . replace ( classesToPrefixRegexJS , ( match ) => {
48- _replacements ++
49- return `${ classPrefix } ${ match } `
50- } )
51- if ( _replacements ) {
52- if ( shouldCount ) {
53- replacements += _replacements
42+ // Helper function to handle both JSX and plain class attributes
43+ function handleClassNameValue ( classNameValueNode , prefixFunc ) {
44+ if ( t . isStringLiteral ( classNameValueNode ) ) {
45+ const stringVal = classNameValueNode . value
46+ const prefixedValue = prefixFunc ( stringVal )
47+ classNameValueNode . value = prefixedValue
48+ } else if ( t . isJSXExpressionContainer ( classNameValueNode ) ) {
49+ const expressionContainer = classNameValueNode . expression
50+ if ( t . isStringLiteral ( expressionContainer ) ) {
51+ const stringVal = expressionContainer . value
52+ const prefixedValue = prefixFunc ( stringVal )
53+ expressionContainer . value = prefixedValue
54+ } else if ( t . isTemplateLiteral ( expressionContainer ) ) {
55+ for ( const expression of expressionContainer . expressions ) {
56+ if ( t . isStringLiteral ( expression ) ) {
57+ const stringVal = expression . value
58+ const prefixedValue = prefixFunc ( stringVal )
59+ expression . value = prefixedValue
60+ }
61+ }
62+ for ( const element of expressionContainer . quasis ) {
63+ const stringVal = element . value . raw
64+ const prefixedValue = prefixFunc ( stringVal )
65+ element . value . raw = prefixedValue
66+
67+ const cstringVal = element . value . cooked
68+ if ( cstringVal ) {
69+ const cprefixedValue = prefixFunc ( cstringVal , false )
70+ element . value . cooked = cprefixedValue
71+ }
5472 }
55- return replaced
5673 }
57- return stringVal
5874 }
75+ }
76+
77+ // Does the actual prefixing
78+ let replacements = 0
79+ const prefixFunc = ( stringVal : string , shouldCount = true ) => {
80+ classesToPrefixRegexJS . lastIndex = 0
81+ let _replacements = 0
82+ const replaced = stringVal . replace ( classesToPrefixRegexJS , ( match ) => {
83+ _replacements ++
84+ return `${ classPrefix } ${ match } `
85+ } )
86+ if ( _replacements ) {
87+ if ( shouldCount ) {
88+ replacements += _replacements
89+ }
90+ return replaced
91+ }
92+ return stringVal
93+ }
94+
95+ // Helper function to handle TemplateLiteral nodes
96+ function handleTemplateLiteral ( templateLiteralNode ) {
97+ for ( const expression of templateLiteralNode . expressions ) {
98+ if ( t . isStringLiteral ( expression ) ) {
99+ const stringVal = expression . value
100+ const prefixedValue = prefixFunc ( stringVal )
101+ expression . value = prefixedValue
102+ }
103+ }
104+ for ( const element of templateLiteralNode . quasis ) {
105+ const stringVal = element . value . raw
106+ const prefixedValue = prefixFunc ( stringVal )
107+ element . value . raw = prefixedValue
108+
109+ const cstringVal = element . value . cooked
110+ if ( cstringVal ) {
111+ const cprefixedValue = prefixFunc ( cstringVal , false )
112+ element . value . cooked = cprefixedValue
113+ }
114+ }
115+ }
59116
117+ /**
118+ * Uses a regex to prefix the classes inside className or class strings
119+ */
120+ async function prefixJSX ( code : string , fileName : string ) {
60121 const ast = parse ( code , {
61122 sourceType : "module" ,
62123 plugins : [ "jsx" , "typescript" ] ,
63124 } )
64125
65- traverse . default ( ast , {
126+ btraverse ( ast , {
66127 JSXAttribute ( path ) {
67128 if ( t . isJSXIdentifier ( path . node . name , { name : "className" } ) ) {
68- const classNameValueNode = path . node . value
69- if ( t . isStringLiteral ( classNameValueNode ) ) {
70- const stringVal = classNameValueNode . value
71- const prefixedValue = prefixFunc ( stringVal )
129+ const classNameValueNode : t . Node = path . node . value
130+ handleClassNameValue ( classNameValueNode , prefixFunc )
131+ }
132+ } ,
133+ ObjectProperty ( path ) {
134+ if (
135+ t . isIdentifier ( path . node . key , { name : "className" } ) ||
136+ t . isIdentifier ( path . node . key , { name : "class" } )
137+ ) {
138+ if ( t . isStringLiteral ( path . node . value ) ) {
139+ const prefixedValue = prefixFunc ( path . node . value . value )
72140 path . node . value = t . stringLiteral ( prefixedValue )
73- } else if ( t . isJSXExpressionContainer ( classNameValueNode ) ) {
74- const expressionContainer = classNameValueNode . expression
75- if ( t . isStringLiteral ( expressionContainer ) ) {
76- const stringVal = expressionContainer . value
77- const prefixedValue = prefixFunc ( stringVal )
78- expressionContainer . value = prefixedValue
79- } else if ( t . isTemplateLiteral ( expressionContainer ) ) {
80- for ( const expression of expressionContainer . expressions ) {
81- if ( t . isStringLiteral ( expression ) ) {
82- const stringVal = expression . value
83- const prefixedValue = prefixFunc ( stringVal )
84- expression . value = prefixedValue
85- }
86- }
87- for ( const element of expressionContainer . quasis ) {
88- const stringVal = element . value . raw
89- const prefixedValue = prefixFunc ( stringVal )
90- element . value . raw = prefixedValue
91-
92- const cstringVal = element . value . cooked
93- if ( cstringVal ) {
94- const cprefixedValue = prefixFunc (
95- cstringVal ,
96- false ,
97- )
98- element . value . cooked = cprefixedValue
99- }
100- }
101- }
102141 }
103142 }
104143 } ,
144+
105145 // Handle TypeScript properties like `this.className = 'some-class'`
106146 ClassProperty ( path ) {
107147 if ( t . isIdentifier ( path . node . key , { name : "className" } ) ) {
@@ -117,9 +157,84 @@ async function prefixJSX(code: string, fileName: string) {
117157 }
118158 }
119159 } ,
160+
161+ // Handle JSX attributes in the transpiled React.createElement calls
162+ CallExpression ( path ) {
163+ const callee = path . get ( "callee" )
164+ // Check if it's a call to jsxRuntimeExports.jsx or jsxRuntimeExports.jsxs
165+ if (
166+ callee . isMemberExpression ( ) &&
167+ t . isIdentifier ( callee . node . object , {
168+ name : "jsxRuntimeExports" ,
169+ } ) &&
170+ ( t . isIdentifier ( callee . node . property , { name : "jsx" } ) ||
171+ t . isIdentifier ( callee . node . property , { name : "jsxs" } ) )
172+ ) {
173+ const args = path . get ( "arguments" )
174+ if ( args . length > 1 ) {
175+ const props = args [ 1 ]
176+ if ( props . isObjectExpression ( ) ) {
177+ // biome-ignore lint/complexity/noForEach: <explanation>
178+ props . get ( "properties" ) . forEach ( ( propPath ) => {
179+ if ( t . isObjectProperty ( propPath . node ) ) {
180+ const key = propPath . get ( "key" )
181+ if ( key . isIdentifier ( { name : "className" } ) ) {
182+ const value = propPath . get ( "value" )
183+ if ( value . isStringLiteral ( ) ) {
184+ const stringVal = value . node . value
185+ const prefixedValue =
186+ prefixFunc ( stringVal )
187+ value . replaceWith (
188+ t . stringLiteral ( prefixedValue ) ,
189+ )
190+ } else if ( value . isTemplateLiteral ( ) ) {
191+ handleTemplateLiteral ( value . node )
192+ }
193+ }
194+ }
195+ } )
196+ }
197+ }
198+ }
199+
200+ // handle React.createElement calls - untested
201+ if (
202+ callee . isIdentifier ( { name : "React" } ) ||
203+ callee . isIdentifier ( { name : "createElement" } )
204+ ) {
205+ const args = path . get ( "arguments" )
206+ if ( args . length > 1 ) {
207+ const props = args [ 1 ] // props is the second argument
208+ if ( props . isObjectExpression ( ) ) {
209+ // biome-ignore lint/complexity/noForEach: <explanation>
210+ props . get ( "properties" ) . forEach ( ( propPath ) => {
211+ if ( t . isObjectProperty ( propPath . node ) ) {
212+ const key = propPath . get ( "key" )
213+ if (
214+ key . isIdentifier ( { name : "className" } ) ||
215+ key . isIdentifier ( { name : "class" } )
216+ ) {
217+ const value = propPath . get ( "value" )
218+ if ( value . isStringLiteral ( ) ) {
219+ const stringVal = value . node . value
220+ const prefixedValue =
221+ prefixFunc ( stringVal )
222+ value . replaceWith (
223+ t . stringLiteral ( prefixedValue ) ,
224+ )
225+ } else if ( value . isTemplateLiteral ( ) ) {
226+ handleTemplateLiteral ( value . node )
227+ }
228+ }
229+ }
230+ } )
231+ }
232+ }
233+ }
234+ } ,
120235 } )
121236
122- const updatedCode = generate . default ( ast ) . code as string
237+ const updatedCode = bgenerate ( ast ) . code as string
123238 if ( replacements ) {
124239 console . log (
125240 "\x1b[33m%s\x1b[0m" ,
@@ -133,7 +248,7 @@ async function prefixJSX(code: string, fileName: string) {
133248/**
134249 * Rollup plugin that prefixes the classes in the JS(X) and TS(X) files.
135250 */
136- export function classPrefixerPlugin ( {
251+ export default function classPrefixerPlugin ( {
137252 prefix,
138253 classes,
139254 jsFiles = [ "js" , "jsx" , "ts" , "tsx" ] ,
@@ -148,7 +263,7 @@ export function classPrefixerPlugin({
148263 classesToBePrefix = classes
149264 jsFilePostfixes = jsFiles
150265 classesToPrefixRegexJS = new RegExp (
151- `\b(${ classesToBePrefix . join ( "|" ) } )\b` ,
266+ `\\ b(${ classesToBePrefix . join ( "|" ) } )\ \b` ,
152267 "g" ,
153268 )
154269 classesToPrefixRegexCSS = new RegExp (
@@ -163,6 +278,8 @@ export function classPrefixerPlugin({
163278 return null
164279 }
165280
281+ replacements = 0
282+
166283 const ext = id . split ( "." ) . pop ( )
167284 if ( ext && jsFilePostfixes . includes ( ext ) ) {
168285 const data = await fs . readFile ( id , "utf-8" )
0 commit comments