@@ -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,67 @@ 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+ "COMPONENT_NAME_2" ,
515+ "STORE_IDENTIFIER" ,
516+ "HOOK_USAGE" ,
517+ "HOOK_IDENTIFIER" ,
518+ "BODY" ,
519+ "COMPONENT_NAME" ,
520+ "STORE_IDENTIFIER" ,
521+ ] ) ,
522+ placeholderPattern : false ,
523+ }
524+ ) ;
525+
384526function wrapInTryFinally (
385527 t : typeof BabelTypes ,
386528 path : NodePath < FunctionLike > ,
387529 state : PluginPass ,
388- hookUsage : HookUsage
530+ hookUsage : HookUsage ,
531+ componentName : string ,
532+ isDebug : boolean
389533) : BabelTypes . BlockStatement {
390534 const stopTrackingIdentifier = path . scope . generateUidIdentifier ( "effect" ) ;
391535
392- return t . blockStatement (
393- tryCatchTemplate ( {
536+ if ( isDebug ) {
537+ const statements = debugTryCatchTemplate ( {
538+ COMPONENT_NAME : t . stringLiteral ( componentName ) ,
394539 STORE_IDENTIFIER : stopTrackingIdentifier ,
395540 HOOK_IDENTIFIER : get ( state , getHookIdentifier ) ( ) ,
396541 HOOK_USAGE : hookUsage ,
397542 BODY : t . isBlockStatement ( path . node . body )
398- ? path . node . body . body // TODO: Is it okay to elide the block statement here?
543+ ? path . node . body . body
399544 : t . returnStatement ( path . node . body ) ,
400- } )
401- ) ;
545+ } ) ;
546+ return t . blockStatement ( statements ) ;
547+ } else {
548+ const statements = tryCatchTemplate ( {
549+ STORE_IDENTIFIER : stopTrackingIdentifier ,
550+ HOOK_IDENTIFIER : get ( state , getHookIdentifier ) ( ) ,
551+ HOOK_USAGE : hookUsage ,
552+ BODY : t . isBlockStatement ( path . node . body )
553+ ? path . node . body . body
554+ : t . returnStatement ( path . node . body ) ,
555+ } ) ;
556+ return t . blockStatement ( statements ) ;
557+ }
402558}
403559
404560function prependUseSignals < T extends FunctionLike > (
@@ -426,7 +582,8 @@ function transformFunction(
426582 options : PluginOptions ,
427583 path : NodePath < FunctionLike > ,
428584 functionName : string | null ,
429- state : PluginPass
585+ state : PluginPass ,
586+ filename : string
430587) {
431588 const isHook = isCustomHookName ( functionName ) ;
432589 const isComponent = isComponentName ( functionName ) ;
@@ -440,7 +597,14 @@ function transformFunction(
440597
441598 let newBody : BabelTypes . BlockStatement ;
442599 if ( hookUsage !== UNMANAGED ) {
443- newBody = wrapInTryFinally ( t , path , state , hookUsage ) ;
600+ newBody = wrapInTryFinally (
601+ t ,
602+ path ,
603+ state ,
604+ hookUsage ,
605+ `${ functionName || "Unknown" } :${ basename ( filename ) } ` ,
606+ isComponent && ! ! options . experimental ?. debug
607+ ) ;
444608 } else {
445609 newBody = prependUseSignals ( t , path , state ) ;
446610 }
@@ -608,6 +772,17 @@ export interface PluginOptions {
608772 */
609773 detectTransformedJSX ?: boolean ;
610774 experimental ?: {
775+ /**
776+ * If set to true the plugin will inject names into all invocations of
777+ *
778+ * - computed/useComputed
779+ * - signal/useSignal
780+ *
781+ * these names hook into @preact/signals-debug.
782+ *
783+ * @default false
784+ */
785+ debug ?: boolean ;
611786 /**
612787 * If set to true, the component body will not be wrapped in a try/finally
613788 * block and instead the next component render or a microtick will stop
@@ -673,7 +848,14 @@ export default function signalsTransform(
673848 }
674849
675850 if ( shouldTransform ( path , functionName , state . opts ) ) {
676- transformFunction ( t , state . opts , path , functionName , state ) ;
851+ transformFunction (
852+ t ,
853+ state . opts ,
854+ path ,
855+ functionName ,
856+ state ,
857+ this . filename || ""
858+ ) ;
677859 log ( true , path , functionName , this . filename ) ;
678860 } else if ( isComponentLike ( path , functionName ) ) {
679861 log ( false , path , functionName , this . filename ) ;
@@ -719,6 +901,19 @@ export default function signalsTransform(
719901 setOnFunctionScope ( path , containsJSX , true , this . filename ) ;
720902 }
721903 }
904+
905+ // Handle signal naming
906+ if ( options . experimental ?. debug && isSignalCall ( path ) ) {
907+ const args = path . get ( "arguments" ) ;
908+
909+ // Only inject name if it doesn't already have one
910+ if ( ! hasNameInOptions ( t , args ) ) {
911+ const variableName = getVariableNameFromDeclarator ( path ) ;
912+ if ( variableName ) {
913+ injectSignalName ( t , path , variableName , this . filename ) ;
914+ }
915+ }
916+ }
722917 } ,
723918
724919 MemberExpression ( path ) {
0 commit comments