7
7
template ,
8
8
} from "@babel/core" ;
9
9
import { isModule , addNamed } from "@babel/helper-module-imports" ;
10
+ import type { VisitNodeObject } from "@babel/traverse" ;
11
+ import debug from "debug" ;
10
12
11
13
interface PluginArgs {
12
14
types : typeof BabelTypes ;
@@ -23,6 +25,11 @@ const maybeUsesSignal = "maybeUsesSignal";
23
25
const containsJSX = "containsJSX" ;
24
26
const alreadyTransformed = "alreadyTransformed" ;
25
27
28
+ const logger = {
29
+ transformed : debug ( "signals:react-transform:transformed" ) ,
30
+ skipped : debug ( "signals:react-transform:skipped" ) ,
31
+ } ;
32
+
26
33
const get = ( pass : PluginPass , name : any ) =>
27
34
pass . get ( `${ dataNamespace } /${ name } ` ) ;
28
35
const set = ( pass : PluginPass , name : string , v : any ) =>
@@ -143,28 +150,11 @@ function getFunctionName(
143
150
return getFunctionNameFromParent ( path . parentPath ) ;
144
151
}
145
152
146
- function fnNameStartsWithCapital (
147
- path : NodePath < FunctionLike > ,
148
- filename : string | undefined
149
- ) : boolean {
150
- const name = getFunctionName ( path ) ;
151
- if ( ! name ) return false ;
152
- if ( name === DefaultExportSymbol ) {
153
- return basename ( filename ) ?. match ( / ^ [ A - Z ] / ) != null ?? false ;
154
- }
155
- return name . match ( / ^ [ A - Z ] / ) != null ;
153
+ function fnNameStartsWithCapital ( name : string | null ) : boolean {
154
+ return name ?. match ( / ^ [ A - Z ] / ) != null ?? false ;
156
155
}
157
- function fnNameStartsWithUse (
158
- path : NodePath < FunctionLike > ,
159
- filename : string | undefined
160
- ) : boolean {
161
- const name = getFunctionName ( path ) ;
162
- if ( ! name ) return false ;
163
- if ( name === DefaultExportSymbol ) {
164
- return basename ( filename ) ?. match ( / ^ u s e [ A - Z ] / ) != null ?? false ;
165
- }
166
-
167
- return name . match ( / ^ u s e [ A - Z ] / ) != null ;
156
+ function fnNameStartsWithUse ( name : string | null ) : boolean {
157
+ return name ?. match ( / ^ u s e [ A - Z ] / ) != null ?? null ;
168
158
}
169
159
170
160
function hasLeadingComment ( path : NodePath , comment : RegExp ) : boolean {
@@ -236,41 +226,36 @@ function isOptedOutOfSignalTracking(path: NodePath | null): boolean {
236
226
237
227
function isComponentFunction (
238
228
path : NodePath < FunctionLike > ,
239
- filename : string | undefined
229
+ functionName : string | null
240
230
) : boolean {
241
231
return (
242
232
getData ( path . scope , containsJSX ) === true && // Function contains JSX
243
- fnNameStartsWithCapital ( path , filename ) // Function name indicates it's a component
233
+ fnNameStartsWithCapital ( functionName ) // Function name indicates it's a component
244
234
) ;
245
235
}
246
236
247
- function isCustomHook (
248
- path : NodePath < FunctionLike > ,
249
- filename : string | undefined
250
- ) : boolean {
251
- return fnNameStartsWithUse ( path , filename ) ; // Function name indicates it's a hook
237
+ function isCustomHook ( functionName : string | null ) : boolean {
238
+ return fnNameStartsWithUse ( functionName ) ; // Function name indicates it's a hook
252
239
}
253
240
254
241
function shouldTransform (
255
242
path : NodePath < FunctionLike > ,
256
- filename : string | undefined ,
243
+ functionName : string | null ,
257
244
options : PluginOptions
258
245
) : boolean {
259
- if ( getData ( path , alreadyTransformed ) === true ) return false ;
260
-
261
246
// Opt-out takes first precedence
262
247
if ( isOptedOutOfSignalTracking ( path ) ) return false ;
263
248
// Opt-in opts in to transformation regardless of mode
264
249
if ( isOptedIntoSignalTracking ( path ) ) return true ;
265
250
266
251
if ( options . mode === "all" ) {
267
- return isComponentFunction ( path , filename ) ;
252
+ return isComponentFunction ( path , functionName ) ;
268
253
}
269
254
270
255
if ( options . mode == null || options . mode === "auto" ) {
271
256
return (
272
257
getData ( path . scope , maybeUsesSignal ) === true && // Function appears to use signals;
273
- ( isComponentFunction ( path , filename ) || isCustomHook ( path , filename ) )
258
+ ( isComponentFunction ( path , functionName ) || isCustomHook ( functionName ) )
274
259
) ;
275
260
}
276
261
@@ -341,11 +326,11 @@ function transformFunction(
341
326
t : typeof BabelTypes ,
342
327
options : PluginOptions ,
343
328
path : NodePath < FunctionLike > ,
344
- filename : string | undefined ,
329
+ functionName : string | null ,
345
330
state : PluginPass
346
331
) {
347
332
let newFunction : FunctionLike ;
348
- if ( isCustomHook ( path , filename ) || options . experimental ?. noTryFinally ) {
333
+ if ( isCustomHook ( functionName ) || options . experimental ?. noTryFinally ) {
349
334
// For custom hooks, we don't need to wrap the function body in a
350
335
// try/finally block because later code in the function's render body could
351
336
// read signals and we want to track and associate those signals with this
@@ -435,10 +420,70 @@ export interface PluginOptions {
435
420
} ;
436
421
}
437
422
423
+ function log (
424
+ transformed : boolean ,
425
+ path : NodePath < FunctionLike > ,
426
+ functionName : string | null ,
427
+ currentFile : string | undefined
428
+ ) {
429
+ if ( ! logger . transformed . enabled && ! logger . skipped . enabled ) return ;
430
+
431
+ let cwd = "" ;
432
+ if ( typeof process !== undefined && typeof process . cwd == "function" ) {
433
+ cwd = process . cwd ( ) . replace ( / \\ ( [ ^ ] ) / g, "/$1" ) ;
434
+ cwd = cwd . endsWith ( "/" ) ? cwd : cwd + "/" ;
435
+ }
436
+
437
+ const relativePath = currentFile ?. replace ( cwd , "" ) ?? "" ;
438
+ const lineNum = path . node . loc ?. start . line ;
439
+ functionName = functionName ?? "<anonymous>" ;
440
+
441
+ if ( transformed ) {
442
+ logger . transformed ( `${ functionName } (${ relativePath } :${ lineNum } )` ) ;
443
+ } else {
444
+ logger . skipped ( `${ functionName } (${ relativePath } :${ lineNum } ) %o` , {
445
+ hasSignals : getData ( path . scope , maybeUsesSignal ) ?? false ,
446
+ hasJSX : getData ( path . scope , containsJSX ) ?? false ,
447
+ } ) ;
448
+ }
449
+ }
450
+
451
+ function isComponentLike (
452
+ path : NodePath < FunctionLike > ,
453
+ functionName : string | null
454
+ ) : boolean {
455
+ return (
456
+ ! getData ( path , alreadyTransformed ) && fnNameStartsWithCapital ( functionName )
457
+ ) ;
458
+ }
459
+
438
460
export default function signalsTransform (
439
461
{ types : t } : PluginArgs ,
440
462
options : PluginOptions
441
463
) : PluginObj {
464
+ // TODO: Consider alternate implementation, where on enter of a function
465
+ // expression, we run our own manual scan the AST to determine if the
466
+ // function uses signals and is a component. This manual scan once upon
467
+ // seeing a function would probably be faster than running an entire
468
+ // babel pass with plugins on components twice.
469
+ const visitFunction : VisitNodeObject < PluginPass , FunctionLike > = {
470
+ exit ( path , state ) {
471
+ if ( getData ( path , alreadyTransformed ) === true ) return false ;
472
+
473
+ let functionName = getFunctionName ( path ) ;
474
+ if ( functionName === DefaultExportSymbol ) {
475
+ functionName = basename ( this . filename ) ?? null ;
476
+ }
477
+
478
+ if ( shouldTransform ( path , functionName , state . opts ) ) {
479
+ transformFunction ( t , state . opts , path , functionName , state ) ;
480
+ log ( true , path , functionName , this . filename ) ;
481
+ } else if ( isComponentLike ( path , functionName ) ) {
482
+ log ( false , path , functionName , this . filename ) ;
483
+ }
484
+ } ,
485
+ } ;
486
+
442
487
return {
443
488
name : "@preact/signals-transform" ,
444
489
visitor : {
@@ -462,42 +507,10 @@ export default function signalsTransform(
462
507
} ,
463
508
} ,
464
509
465
- ArrowFunctionExpression : {
466
- // TODO: Consider alternate implementation, where on enter of a function
467
- // expression, we run our own manual scan the AST to determine if the
468
- // function uses signals and is a component. This manual scan once upon
469
- // seeing a function would probably be faster than running an entire
470
- // babel pass with plugins on components twice.
471
- exit ( path , state ) {
472
- if ( shouldTransform ( path , this . filename , options ) ) {
473
- transformFunction ( t , options , path , this . filename , state ) ;
474
- }
475
- } ,
476
- } ,
477
-
478
- FunctionExpression : {
479
- exit ( path , state ) {
480
- if ( shouldTransform ( path , this . filename , options ) ) {
481
- transformFunction ( t , options , path , this . filename , state ) ;
482
- }
483
- } ,
484
- } ,
485
-
486
- FunctionDeclaration : {
487
- exit ( path , state ) {
488
- if ( shouldTransform ( path , this . filename , options ) ) {
489
- transformFunction ( t , options , path , this . filename , state ) ;
490
- }
491
- } ,
492
- } ,
493
-
494
- ObjectMethod : {
495
- exit ( path , state ) {
496
- if ( shouldTransform ( path , this . filename , options ) ) {
497
- transformFunction ( t , options , path , this . filename , state ) ;
498
- }
499
- } ,
500
- } ,
510
+ ArrowFunctionExpression : visitFunction ,
511
+ FunctionExpression : visitFunction ,
512
+ FunctionDeclaration : visitFunction ,
513
+ ObjectMethod : visitFunction ,
501
514
502
515
MemberExpression ( path ) {
503
516
if ( isValueMemberExpression ( path ) ) {
0 commit comments