1+ /**
2+ * Custom loader that transforms JSX to html-tag-js tag() calls
3+ * This uses Babel's parser/transformer but is lighter than full babel-loader
4+ */
5+ const { parse } = require ( '@babel/parser' ) ;
6+ const traverse = require ( '@babel/traverse' ) . default ;
7+ const generate = require ( '@babel/generator' ) . default ;
8+ const t = require ( '@babel/types' ) ;
9+
10+ module . exports = function htmlTagJsxLoader ( source ) {
11+ const callback = this . async ( ) ;
12+
13+ // Enable caching for this loader
14+ this . cacheable && this . cacheable ( ) ;
15+
16+ try {
17+ // Debug logging - verify loader is running
18+ console . log ( '🔧 Custom JSX loader processing:' , this . resourcePath ) ;
19+
20+ // Determine file type from extension
21+ const isTypeScript = / \. t s x ? $ / . test ( this . resourcePath ) ;
22+ const isJavaScript = / \. ( j s x ? | m j s ) $ / . test ( this . resourcePath ) ;
23+
24+ // Quick check: if no JSX syntax at all, pass through unchanged
25+ // Look for JSX opening tags: < followed by a letter or uppercase
26+ const hasJSXLike = / < [ A - Z a - z ] / . test ( source ) ;
27+ if ( ! hasJSXLike ) {
28+ return callback ( null , source ) ;
29+ }
30+
31+ // Parse with appropriate plugins
32+ const parserPlugins = [ 'jsx' , '@babel/plugin-transform-runtime' , '@babel/plugin-transform-block-scoping' ] ;
33+ if ( isTypeScript ) {
34+ parserPlugins . push ( 'typescript' ) ;
35+ }
36+
37+ const ast = parse ( source , {
38+ sourceType : 'module' ,
39+ plugins : parserPlugins ,
40+ } ) ;
41+
42+ // Track if we need to add the import
43+ let needsTagImport = false ;
44+ let hasJSX = false ;
45+ const hasExistingImport = source . includes ( 'import tag from \'html-tag-js\'' ) ||
46+ source . includes ( 'import tag from "html-tag-js"' ) ;
47+
48+ // Transform JSX elements
49+ traverse ( ast , {
50+ JSXFragment ( path ) {
51+ hasJSX = true ;
52+ needsTagImport = true ;
53+ const { node } = path ;
54+ const { children : childrenNode } = node ;
55+
56+ const children = [ ] ;
57+ populateChildren ( childrenNode , children , t ) ;
58+ const arrayExpression = t . arrayExpression ( children ) ;
59+ path . replaceWith ( arrayExpression ) ;
60+ } ,
61+
62+ JSXElement ( path ) {
63+ hasJSX = true ;
64+ needsTagImport = true ;
65+ const { node } = path ;
66+ const {
67+ openingElement : el ,
68+ children : childrenNode ,
69+ } = node ;
70+
71+ let { name : tagName } = el . name ;
72+ const { attributes } = el ;
73+
74+ let id ;
75+ let className ;
76+ const on = [ ] ;
77+ const args = [ ] ;
78+ const attrs = [ ] ;
79+ const children = [ ] ;
80+ const options = [ ] ;
81+ const events = { } ;
82+ let isComponent = / ^ (?: [ A - Z ] [ a - z A - Z 0 - 9 _ $ ] * | (?: [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * \. ) + [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * ) $ / . test ( tagName ) ;
83+
84+ if ( el . name . type === 'JSXMemberExpression' ) {
85+ const { object, property } = el . name ;
86+ tagName = `${ object . name } .${ property . name } ` ;
87+ isComponent = true ;
88+ }
89+
90+ populateChildren ( childrenNode , children , t ) ;
91+
92+ for ( const attr of attributes ) {
93+ if ( attr . type === 'JSXSpreadAttribute' ) {
94+ if ( isComponent ) {
95+ attrs . push ( t . spreadElement ( attr . argument ) ) ;
96+ } else {
97+ options . push ( t . spreadElement ( attr . argument ) ) ;
98+ }
99+ continue ;
100+ }
101+
102+ let { name, namespace } = attr . name ;
103+
104+ if ( ! isComponent ) {
105+ if ( name === 'id' ) {
106+ if ( attr . value && attr . value . type === 'StringLiteral' ) {
107+ id = attr . value ;
108+ } else if ( attr . value && attr . value . type === 'JSXExpressionContainer' ) {
109+ id = attr . value . expression ;
110+ }
111+ continue ;
112+ }
113+
114+ if ( [ 'class' , 'className' ] . includes ( name ) ) {
115+ if ( attr . value && attr . value . type === 'StringLiteral' ) {
116+ className = attr . value ;
117+ } else if ( attr . value && attr . value . type === 'JSXExpressionContainer' ) {
118+ className = attr . value . expression ;
119+ }
120+ continue ;
121+ }
122+ }
123+
124+ if ( namespace ) {
125+ namespace = namespace . name ;
126+ name = name . name ;
127+ }
128+
129+ if ( ! attr . value ) {
130+ attrs . push ( t . objectProperty (
131+ t . stringLiteral ( name ) ,
132+ t . stringLiteral ( '' ) ,
133+ ) ) ;
134+ continue ;
135+ }
136+
137+ const { type } = attr . value ;
138+ const isAttr = / - / . test ( name ) ;
139+ let value ;
140+
141+ if ( type === 'StringLiteral' ) {
142+ value = attr . value ;
143+ } else {
144+ value = attr . value . expression ;
145+ }
146+
147+ if ( namespace ) {
148+ if ( ! [ 'on' , 'once' , 'off' ] . includes ( namespace ) ) {
149+ attrs . push ( t . objectProperty (
150+ t . stringLiteral ( namespace === 'attr' ? name : `${ namespace } :${ name } ` ) ,
151+ value ,
152+ ) ) ;
153+ continue ;
154+ }
155+
156+ if ( namespace === 'off' ) continue ;
157+
158+ if ( ! events [ name ] ) {
159+ events [ name ] = [ ] ;
160+ on . push ( t . objectProperty (
161+ t . stringLiteral ( name ) ,
162+ t . arrayExpression ( events [ name ] ) ,
163+ ) ) ;
164+ }
165+
166+ events [ name ] . push ( value ) ;
167+ continue ;
168+ }
169+
170+ if ( isAttr ) {
171+ const attrRegex = / ^ a t t r - ( .+ ) / ;
172+ if ( attrRegex . test ( name ) ) {
173+ [ , name ] = attrRegex . exec ( name ) ;
174+ }
175+
176+ attrs . push ( t . objectProperty (
177+ t . stringLiteral ( name ) ,
178+ value ,
179+ ) ) ;
180+ continue ;
181+ }
182+
183+ ( isComponent ? attrs : options )
184+ . unshift ( t . objectProperty (
185+ t . identifier ( name ) ,
186+ value ,
187+ ) ) ;
188+ }
189+
190+ if ( isComponent ) {
191+ args . push ( t . identifier ( tagName ) ) ;
192+
193+ if ( on . length > 0 ) {
194+ attrs . push (
195+ t . objectProperty (
196+ t . identifier ( 'on' ) ,
197+ t . objectExpression ( on ) ,
198+ )
199+ ) ;
200+ }
201+
202+ if ( attrs . length > 0 ) {
203+ args . push ( t . objectExpression ( attrs ) ) ;
204+ }
205+
206+ if ( children . length > 0 ) {
207+ args . push ( t . arrayExpression ( children ) ) ;
208+ }
209+ } else {
210+ args . push ( t . stringLiteral ( tagName ) ) ;
211+
212+ if ( on . length > 0 ) {
213+ options . push (
214+ t . objectProperty (
215+ t . identifier ( 'on' ) ,
216+ t . objectExpression ( on ) ,
217+ )
218+ ) ;
219+ }
220+
221+ if ( attrs . length > 0 ) {
222+ options . push (
223+ t . objectProperty (
224+ t . identifier ( 'attr' ) ,
225+ t . objectExpression ( attrs ) ,
226+ )
227+ ) ;
228+ }
229+
230+ if ( id || className ) {
231+ if ( className ) {
232+ args . push ( className ) ;
233+ } else if ( id ) {
234+ args . push ( t . nullLiteral ( ) ) ;
235+ }
236+
237+ if ( id ) {
238+ args . push ( id ) ;
239+ }
240+ }
241+
242+ if ( children . length ) {
243+ args . push ( t . arrayExpression ( children ) ) ;
244+ }
245+
246+ if ( options . length ) {
247+ args . push ( t . objectExpression ( options ) ) ;
248+ }
249+ }
250+
251+ const identifier = t . identifier ( 'tag' ) ;
252+ const callExpression = t . callExpression ( identifier , args ) ;
253+ path . replaceWith ( callExpression ) ;
254+ } ,
255+ } ) ;
256+
257+ // If no JSX was found, return original source
258+ if ( ! hasJSX ) {
259+ return callback ( null , source ) ;
260+ }
261+
262+ // Generate the transformed code
263+ const output = generate ( ast , {
264+ sourceMaps : true ,
265+ sourceFileName : this . resourcePath ,
266+ retainLines : false ,
267+ compact : false
268+ } , source ) ;
269+
270+ // Add import if needed
271+ if ( needsTagImport && ! hasExistingImport ) {
272+ output . code = `import tag from 'html-tag-js';\n${ output . code } ` ;
273+ }
274+
275+ callback ( null , output . code , output . map ) ;
276+ } catch ( error ) {
277+ callback ( error ) ;
278+ }
279+ } ;
280+
281+ /**
282+ * Parse node to expression
283+ */
284+ function parseNode ( types , node ) {
285+ const { type } = node ;
286+
287+ if ( type === 'JSXText' ) {
288+ return types . stringLiteral ( node . value ) ;
289+ }
290+
291+ if ( type === 'JSXElement' ) {
292+ return node ;
293+ }
294+
295+ const { expression } = node ;
296+ const invalidExpressions = [ 'JSXEmptyExpression' ] ;
297+
298+ if ( invalidExpressions . includes ( expression . type ) ) {
299+ return null ;
300+ }
301+
302+ return expression ;
303+ }
304+
305+ /**
306+ * Populate children
307+ */
308+ function populateChildren ( childrenNode , children , t ) {
309+ for ( let node of childrenNode ) {
310+ node = parseNode ( t , node ) ;
311+ if ( ! node ) continue ;
312+ children . push ( node ) ;
313+ }
314+ }
0 commit comments