@@ -361,6 +361,119 @@ function isJSXAlternativeCall(
361
361
return false ;
362
362
}
363
363
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
+
364
477
function hasValuePropertyInPattern ( pattern : BabelTypes . ObjectPattern ) : boolean {
365
478
for ( const property of pattern . properties ) {
366
479
if ( BabelTypes . isObjectProperty ( property ) ) {
@@ -381,24 +494,66 @@ try {
381
494
STORE_IDENTIFIER.f();
382
495
}` ;
383
496
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
+
384
525
function wrapInTryFinally (
385
526
t : typeof BabelTypes ,
386
527
path : NodePath < FunctionLike > ,
387
528
state : PluginPass ,
388
- hookUsage : HookUsage
529
+ hookUsage : HookUsage ,
530
+ componentName : string ,
531
+ isDebug : boolean
389
532
) : BabelTypes . BlockStatement {
390
533
const stopTrackingIdentifier = path . scope . generateUidIdentifier ( "effect" ) ;
391
534
392
- return t . blockStatement (
393
- tryCatchTemplate ( {
535
+ if ( isDebug ) {
536
+ const statements = debugTryCatchTemplate ( {
537
+ COMPONENT_NAME : t . stringLiteral ( componentName ) ,
394
538
STORE_IDENTIFIER : stopTrackingIdentifier ,
395
539
HOOK_IDENTIFIER : get ( state , getHookIdentifier ) ( ) ,
396
540
HOOK_USAGE : hookUsage ,
397
541
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
399
543
: 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
+ }
402
557
}
403
558
404
559
function prependUseSignals < T extends FunctionLike > (
@@ -426,7 +581,8 @@ function transformFunction(
426
581
options : PluginOptions ,
427
582
path : NodePath < FunctionLike > ,
428
583
functionName : string | null ,
429
- state : PluginPass
584
+ state : PluginPass ,
585
+ filename : string
430
586
) {
431
587
const isHook = isCustomHookName ( functionName ) ;
432
588
const isComponent = isComponentName ( functionName ) ;
@@ -440,7 +596,14 @@ function transformFunction(
440
596
441
597
let newBody : BabelTypes . BlockStatement ;
442
598
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
+ ) ;
444
607
} else {
445
608
newBody = prependUseSignals ( t , path , state ) ;
446
609
}
@@ -608,6 +771,17 @@ export interface PluginOptions {
608
771
*/
609
772
detectTransformedJSX ?: boolean ;
610
773
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 ;
611
785
/**
612
786
* If set to true, the component body will not be wrapped in a try/finally
613
787
* block and instead the next component render or a microtick will stop
@@ -673,7 +847,14 @@ export default function signalsTransform(
673
847
}
674
848
675
849
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
+ ) ;
677
858
log ( true , path , functionName , this . filename ) ;
678
859
} else if ( isComponentLike ( path , functionName ) ) {
679
860
log ( false , path , functionName , this . filename ) ;
@@ -719,6 +900,19 @@ export default function signalsTransform(
719
900
setOnFunctionScope ( path , containsJSX , true , this . filename ) ;
720
901
}
721
902
}
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
+ }
722
916
} ,
723
917
724
918
MemberExpression ( path ) {
0 commit comments