@@ -361,6 +361,119 @@ function isJSXAlternativeCall(
361361 return false ;
362362}
363363
364+ function isSignalCall ( path : NodePath < BabelTypes . CallExpression > ) : boolean {
365+ const callee = path . get ( "callee" ) ;
366+
367+ // Check direct function calls like signal(), computed(), useSignal(), useComputed()
368+ if ( callee . isIdentifier ( ) ) {
369+ const name = callee . node . name ;
370+ return (
371+ name === "signal" ||
372+ name === "computed" ||
373+ name === "useSignal" ||
374+ name === "useComputed"
375+ ) ;
376+ }
377+
378+ return false ;
379+ }
380+
381+ function getVariableNameFromDeclarator (
382+ path : NodePath < BabelTypes . CallExpression >
383+ ) : string | null {
384+ // Walk up the AST to find a variable declarator
385+ let currentPath : NodePath | null = path ;
386+ while ( currentPath ) {
387+ if (
388+ currentPath . isVariableDeclarator ( ) &&
389+ currentPath . node . id . type === "Identifier"
390+ ) {
391+ return currentPath . node . id . name ;
392+ }
393+ currentPath = currentPath . parentPath ;
394+ }
395+ return null ;
396+ }
397+
398+ function hasNameInOptions (
399+ t : typeof BabelTypes ,
400+ args : NodePath <
401+ | BabelTypes . Expression
402+ | BabelTypes . SpreadElement
403+ | BabelTypes . JSXNamespacedName
404+ | BabelTypes . ArgumentPlaceholder
405+ > [ ]
406+ ) : boolean {
407+ // Check if there's a second argument with a name property
408+ if ( args . length >= 2 ) {
409+ const optionsArg = args [ 1 ] ;
410+ if ( optionsArg . isObjectExpression ( ) ) {
411+ return optionsArg . node . properties . some ( prop => {
412+ if ( t . isObjectProperty ( prop ) && ! prop . computed ) {
413+ if ( t . isIdentifier ( prop . key , { name : "name" } ) ) {
414+ return true ;
415+ }
416+ if ( t . isStringLiteral ( prop . key ) && prop . key . value === "name" ) {
417+ return true ;
418+ }
419+ }
420+ return false ;
421+ } ) ;
422+ }
423+ }
424+ return false ;
425+ }
426+
427+ function injectSignalName (
428+ t : typeof BabelTypes ,
429+ path : NodePath < BabelTypes . CallExpression > ,
430+ variableName : string ,
431+ filename : string | undefined
432+ ) : void {
433+ const args = path . get ( "arguments" ) ;
434+
435+ // Create enhanced name with filename and line number
436+ let nameValue = variableName ;
437+ if ( filename ) {
438+ const baseName = basename ( filename ) ;
439+ const lineNumber = path . node . loc ?. start . line ;
440+ if ( baseName && lineNumber ) {
441+ nameValue = `${ variableName } (${ baseName } :${ lineNumber } )` ;
442+ }
443+ }
444+
445+ const name = t . stringLiteral ( nameValue ) ;
446+
447+ if ( args . length === 0 ) {
448+ // No arguments, add both value and options
449+ const nameOption = t . objectExpression ( [
450+ t . objectProperty ( t . identifier ( "name" ) , name ) ,
451+ ] ) ;
452+ path . node . arguments . push ( t . identifier ( "undefined" ) , nameOption ) ;
453+ } else if ( args . length === 1 ) {
454+ // One argument (value), add options object
455+ const nameOption = t . objectExpression ( [
456+ t . objectProperty ( t . identifier ( "name" ) , name ) ,
457+ ] ) ;
458+ path . node . arguments . push ( nameOption ) ;
459+ } else if ( args . length >= 2 ) {
460+ // Two or more arguments, modify existing options object
461+ const optionsArg = args [ 1 ] ;
462+ if ( optionsArg . isObjectExpression ( ) ) {
463+ // Add name property to existing options object
464+ optionsArg . node . properties . push (
465+ t . objectProperty ( t . identifier ( "name" ) , name )
466+ ) ;
467+ } else {
468+ // Replace second argument with options object containing name
469+ const nameOption = t . objectExpression ( [
470+ t . objectProperty ( t . identifier ( "name" ) , name ) ,
471+ ] ) ;
472+ args [ 1 ] . replaceWith ( nameOption ) ;
473+ }
474+ }
475+ }
476+
364477function hasValuePropertyInPattern ( pattern : BabelTypes . ObjectPattern ) : boolean {
365478 for ( const property of pattern . properties ) {
366479 if ( BabelTypes . isObjectProperty ( property ) ) {
@@ -381,24 +494,66 @@ try {
381494 STORE_IDENTIFIER.f();
382495}` ;
383496
497+ const debugTryCatchTemplate = template . statements (
498+ `var STORE_IDENTIFIER = HOOK_IDENTIFIER(HOOK_USAGE);
499+ try {
500+ if (window.__PREACT_SIGNALS_DEVTOOLS__) {
501+ window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(
502+ COMPONENT_NAME
503+ );
504+ }
505+ BODY
506+ } finally {
507+ STORE_IDENTIFIER.f();
508+ if (window.__PREACT_SIGNALS_DEVTOOLS__) {
509+ window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent();
510+ }
511+ }` ,
512+ {
513+ placeholderWhitelist : new Set ( [
514+ "STORE_IDENTIFIER" ,
515+ "HOOK_USAGE" ,
516+ "HOOK_IDENTIFIER" ,
517+ "BODY" ,
518+ "COMPONENT_NAME" ,
519+ "STORE_IDENTIFIER" ,
520+ ] ) ,
521+ placeholderPattern : false ,
522+ }
523+ ) ;
524+
384525function wrapInTryFinally (
385526 t : typeof BabelTypes ,
386527 path : NodePath < FunctionLike > ,
387528 state : PluginPass ,
388- hookUsage : HookUsage
529+ hookUsage : HookUsage ,
530+ componentName : string ,
531+ isDebug : boolean
389532) : BabelTypes . BlockStatement {
390533 const stopTrackingIdentifier = path . scope . generateUidIdentifier ( "effect" ) ;
391534
392- return t . blockStatement (
393- tryCatchTemplate ( {
535+ if ( isDebug ) {
536+ const statements = debugTryCatchTemplate ( {
537+ COMPONENT_NAME : t . stringLiteral ( componentName ) ,
394538 STORE_IDENTIFIER : stopTrackingIdentifier ,
395539 HOOK_IDENTIFIER : get ( state , getHookIdentifier ) ( ) ,
396540 HOOK_USAGE : hookUsage ,
397541 BODY : t . isBlockStatement ( path . node . body )
398- ? path . node . body . body // TODO: Is it okay to elide the block statement here?
542+ ? path . node . body . body
399543 : t . returnStatement ( path . node . body ) ,
400- } )
401- ) ;
544+ } ) ;
545+ return t . blockStatement ( statements ) ;
546+ } else {
547+ const statements = tryCatchTemplate ( {
548+ STORE_IDENTIFIER : stopTrackingIdentifier ,
549+ HOOK_IDENTIFIER : get ( state , getHookIdentifier ) ( ) ,
550+ HOOK_USAGE : hookUsage ,
551+ BODY : t . isBlockStatement ( path . node . body )
552+ ? path . node . body . body
553+ : t . returnStatement ( path . node . body ) ,
554+ } ) ;
555+ return t . blockStatement ( statements ) ;
556+ }
402557}
403558
404559function prependUseSignals < T extends FunctionLike > (
@@ -426,7 +581,8 @@ function transformFunction(
426581 options : PluginOptions ,
427582 path : NodePath < FunctionLike > ,
428583 functionName : string | null ,
429- state : PluginPass
584+ state : PluginPass ,
585+ filename : string
430586) {
431587 const isHook = isCustomHookName ( functionName ) ;
432588 const isComponent = isComponentName ( functionName ) ;
@@ -440,7 +596,14 @@ function transformFunction(
440596
441597 let newBody : BabelTypes . BlockStatement ;
442598 if ( hookUsage !== UNMANAGED ) {
443- newBody = wrapInTryFinally ( t , path , state , hookUsage ) ;
599+ newBody = wrapInTryFinally (
600+ t ,
601+ path ,
602+ state ,
603+ hookUsage ,
604+ `${ functionName || "Unknown" } :${ basename ( filename ) } ` ,
605+ isComponent && ! ! options . experimental ?. debug
606+ ) ;
444607 } else {
445608 newBody = prependUseSignals ( t , path , state ) ;
446609 }
@@ -608,6 +771,17 @@ export interface PluginOptions {
608771 */
609772 detectTransformedJSX ?: boolean ;
610773 experimental ?: {
774+ /**
775+ * If set to true the plugin will inject names into all invocations of
776+ *
777+ * - computed/useComputed
778+ * - signal/useSignal
779+ *
780+ * these names hook into @preact/signals-debug.
781+ *
782+ * @default false
783+ */
784+ debug ?: boolean ;
611785 /**
612786 * If set to true, the component body will not be wrapped in a try/finally
613787 * block and instead the next component render or a microtick will stop
@@ -673,7 +847,14 @@ export default function signalsTransform(
673847 }
674848
675849 if ( shouldTransform ( path , functionName , state . opts ) ) {
676- transformFunction ( t , state . opts , path , functionName , state ) ;
850+ transformFunction (
851+ t ,
852+ state . opts ,
853+ path ,
854+ functionName ,
855+ state ,
856+ this . filename || ""
857+ ) ;
677858 log ( true , path , functionName , this . filename ) ;
678859 } else if ( isComponentLike ( path , functionName ) ) {
679860 log ( false , path , functionName , this . filename ) ;
@@ -719,6 +900,19 @@ export default function signalsTransform(
719900 setOnFunctionScope ( path , containsJSX , true , this . filename ) ;
720901 }
721902 }
903+
904+ // Handle signal naming
905+ if ( options . experimental ?. debug && isSignalCall ( path ) ) {
906+ const args = path . get ( "arguments" ) ;
907+
908+ // Only inject name if it doesn't already have one
909+ if ( ! hasNameInOptions ( t , args ) ) {
910+ const variableName = getVariableNameFromDeclarator ( path ) ;
911+ if ( variableName ) {
912+ injectSignalName ( t , path , variableName , this . filename ) ;
913+ }
914+ }
915+ }
722916 } ,
723917
724918 MemberExpression ( path ) {
0 commit comments