@@ -24,6 +24,8 @@ const getHookIdentifier = "getHookIdentifier";
24
24
const maybeUsesSignal = "maybeUsesSignal" ;
25
25
const containsJSX = "containsJSX" ;
26
26
const alreadyTransformed = "alreadyTransformed" ;
27
+ const jsxIdentifiers = "jsxIdentifiers" ;
28
+ const jsxObjects = "jsxObjects" ;
27
29
28
30
const UNMANAGED = "0" ;
29
31
const MANAGED_COMPONENT = "1" ;
@@ -330,6 +332,35 @@ function isValueMemberExpression(
330
332
) ;
331
333
}
332
334
335
+ function isJSXAlternativeCall (
336
+ path : NodePath < BabelTypes . CallExpression > ,
337
+ state : PluginPass
338
+ ) : boolean {
339
+ const jsxIdentifierSet = get ( state , jsxIdentifiers ) as Set < string > ;
340
+ const jsxObjectMap = get ( state , jsxObjects ) as Map < string , string [ ] > ;
341
+ const callee = path . get ( "callee" ) ;
342
+
343
+ // Check direct function calls like _jsx("div", props) or createElement("div", props)
344
+ if ( callee . isIdentifier ( ) ) {
345
+ return jsxIdentifierSet ?. has ( callee . node . name ) ?? false ;
346
+ }
347
+
348
+ // Check member expression calls like React.createElement("div", props) or jsxRuntime.jsx("div", props)
349
+ if ( callee . isMemberExpression ( ) ) {
350
+ const object = callee . get ( "object" ) ;
351
+ const property = callee . get ( "property" ) ;
352
+
353
+ if ( object . isIdentifier ( ) && property . isIdentifier ( ) ) {
354
+ const objectName = object . node . name ;
355
+ const methodName = property . node . name ;
356
+ const allowedMethods = jsxObjectMap ?. get ( objectName ) ;
357
+ return allowedMethods ?. includes ( methodName ) ?? false ;
358
+ }
359
+ }
360
+
361
+ return false ;
362
+ }
363
+
333
364
const tryCatchTemplate = template . statements `var STORE_IDENTIFIER = HOOK_IDENTIFIER(HOOK_USAGE);
334
365
try {
335
366
BODY
@@ -465,6 +496,88 @@ function createImportLazily(
465
496
} ;
466
497
}
467
498
499
+ function detectJSXAlternativeImports (
500
+ path : NodePath < BabelTypes . Program > ,
501
+ state : PluginPass
502
+ ) {
503
+ const jsxIdentifierSet = new Set < string > ( ) ;
504
+ const jsxObjectMap = new Map < string , string [ ] > ( ) ;
505
+
506
+ const jsxPackages = {
507
+ "react/jsx-runtime" : [ "jsx" , "jsxs" ] ,
508
+ "react/jsx-dev-runtime" : [ "jsxDEV" ] ,
509
+ react : [ "createElement" ] ,
510
+ } ;
511
+
512
+ path . traverse ( {
513
+ ImportDeclaration ( importPath ) {
514
+ const packageName = importPath . node . source . value ;
515
+ const jsxMethods = jsxPackages [ packageName as keyof typeof jsxPackages ] ;
516
+
517
+ if ( ! jsxMethods ) {
518
+ return ;
519
+ }
520
+
521
+ for ( const specifier of importPath . node . specifiers ) {
522
+ if (
523
+ specifier . type === "ImportSpecifier" &&
524
+ specifier . imported . type === "Identifier"
525
+ ) {
526
+ // Check if this is a function we care about
527
+ if ( jsxMethods . includes ( specifier . imported . name ) ) {
528
+ jsxIdentifierSet . add ( specifier . local . name ) ;
529
+ }
530
+ } else if ( specifier . type === "ImportDefaultSpecifier" ) {
531
+ // Handle default imports - add to objects map for member access
532
+ jsxObjectMap . set ( specifier . local . name , jsxMethods ) ;
533
+ }
534
+ }
535
+ } ,
536
+ VariableDeclarator ( varPath ) {
537
+ const init = varPath . get ( "init" ) ;
538
+
539
+ if ( init . isCallExpression ( ) ) {
540
+ const callee = init . get ( "callee" ) ;
541
+ const args = init . get ( "arguments" ) ;
542
+
543
+ if (
544
+ callee . isIdentifier ( ) &&
545
+ callee . node . type === "Identifier" &&
546
+ callee . node . name === "require" &&
547
+ args . length > 0 &&
548
+ args [ 0 ] . isStringLiteral ( )
549
+ ) {
550
+ const packageName = args [ 0 ] . node . value ;
551
+ const jsxMethods =
552
+ jsxPackages [ packageName as keyof typeof jsxPackages ] ;
553
+
554
+ if ( jsxMethods ) {
555
+ if ( varPath . node . id . type === "Identifier" ) {
556
+ // Handle CJS require like: const React = require("react")
557
+ jsxObjectMap . set ( varPath . node . id . name , jsxMethods ) ;
558
+ } else if ( varPath . node . id . type === "ObjectPattern" ) {
559
+ // Handle destructured CJS require like: const { createElement } = require("react")
560
+ for ( const prop of varPath . node . id . properties ) {
561
+ if (
562
+ prop . type === "ObjectProperty" &&
563
+ prop . key . type === "Identifier" &&
564
+ prop . value . type === "Identifier" &&
565
+ jsxMethods . includes ( prop . key . name )
566
+ ) {
567
+ jsxIdentifierSet . add ( prop . value . name ) ;
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
573
+ }
574
+ } ,
575
+ } ) ;
576
+
577
+ set ( state , jsxIdentifiers , jsxIdentifierSet ) ;
578
+ set ( state , jsxObjects , jsxObjectMap ) ;
579
+ }
580
+
468
581
export interface PluginOptions {
469
582
/**
470
583
* Specify the mode to use:
@@ -475,6 +588,12 @@ export interface PluginOptions {
475
588
mode ?: "auto" | "manual" | "all" ;
476
589
/** Specify a custom package to import the `useSignals` hook from. */
477
590
importSource ?: string ;
591
+ /**
592
+ * Detect JSX elements created using alternative methods like jsx-runtime or createElement calls.
593
+ * When enabled, detects patterns from react/jsx-runtime and react packages.
594
+ * @default false
595
+ */
596
+ detectTransformedJSX ?: boolean ;
478
597
experimental ?: {
479
598
/**
480
599
* If set to true, the component body will not be wrapped in a try/finally
@@ -569,6 +688,10 @@ export default function signalsTransform(
569
688
options . importSource ?? defaultImportSource
570
689
)
571
690
) ;
691
+
692
+ if ( options . detectTransformedJSX ) {
693
+ detectJSXAlternativeImports ( path , state ) ;
694
+ }
572
695
} ,
573
696
} ,
574
697
@@ -577,6 +700,14 @@ export default function signalsTransform(
577
700
FunctionDeclaration : visitFunction ,
578
701
ObjectMethod : visitFunction ,
579
702
703
+ CallExpression ( path , state ) {
704
+ if ( options . detectTransformedJSX ) {
705
+ if ( isJSXAlternativeCall ( path , state ) ) {
706
+ setOnFunctionScope ( path , containsJSX , true , this . filename ) ;
707
+ }
708
+ }
709
+ } ,
710
+
580
711
MemberExpression ( path ) {
581
712
if ( isValueMemberExpression ( path ) ) {
582
713
setOnFunctionScope ( path , maybeUsesSignal , true , this . filename ) ;
0 commit comments