@@ -24,6 +24,8 @@ const getHookIdentifier = "getHookIdentifier";
2424const maybeUsesSignal = "maybeUsesSignal" ;
2525const containsJSX = "containsJSX" ;
2626const alreadyTransformed = "alreadyTransformed" ;
27+ const jsxIdentifiers = "jsxIdentifiers" ;
28+ const jsxObjects = "jsxObjects" ;
2729
2830const UNMANAGED = "0" ;
2931const MANAGED_COMPONENT = "1" ;
@@ -330,6 +332,35 @@ function isValueMemberExpression(
330332 ) ;
331333}
332334
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+
333364const tryCatchTemplate = template . statements `var STORE_IDENTIFIER = HOOK_IDENTIFIER(HOOK_USAGE);
334365try {
335366 BODY
@@ -465,6 +496,88 @@ function createImportLazily(
465496 } ;
466497}
467498
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+
468581export interface PluginOptions {
469582 /**
470583 * Specify the mode to use:
@@ -475,6 +588,12 @@ export interface PluginOptions {
475588 mode ?: "auto" | "manual" | "all" ;
476589 /** Specify a custom package to import the `useSignals` hook from. */
477590 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 ;
478597 experimental ?: {
479598 /**
480599 * If set to true, the component body will not be wrapped in a try/finally
@@ -569,6 +688,10 @@ export default function signalsTransform(
569688 options . importSource ?? defaultImportSource
570689 )
571690 ) ;
691+
692+ if ( options . detectTransformedJSX ) {
693+ detectJSXAlternativeImports ( path , state ) ;
694+ }
572695 } ,
573696 } ,
574697
@@ -577,6 +700,14 @@ export default function signalsTransform(
577700 FunctionDeclaration : visitFunction ,
578701 ObjectMethod : visitFunction ,
579702
703+ CallExpression ( path , state ) {
704+ if ( options . detectTransformedJSX ) {
705+ if ( isJSXAlternativeCall ( path , state ) ) {
706+ setOnFunctionScope ( path , containsJSX , true , this . filename ) ;
707+ }
708+ }
709+ } ,
710+
580711 MemberExpression ( path ) {
581712 if ( isValueMemberExpression ( path ) ) {
582713 setOnFunctionScope ( path , maybeUsesSignal , true , this . filename ) ;
0 commit comments