@@ -8,10 +8,6 @@ import {
8
8
} from "@babel/core" ;
9
9
import { isModule , addNamed } from "@babel/helper-module-imports" ;
10
10
11
- // TODO:
12
- // - how to trigger rerenders on attributes change if transform never sees
13
- // `.value`?
14
-
15
11
interface PluginArgs {
16
12
types : typeof BabelTypes ;
17
13
template : typeof BabelTemplate ;
@@ -51,35 +47,125 @@ function setOnFunctionScope(path: NodePath, key: string, value: any) {
51
47
type FunctionLike =
52
48
| BabelTypes . ArrowFunctionExpression
53
49
| BabelTypes . FunctionExpression
54
- | BabelTypes . FunctionDeclaration ;
55
-
56
- function testFunctionName < T extends FunctionLike > (
57
- predicate : ( name : string | null ) => boolean
58
- ) : ( path : NodePath < T > ) => boolean {
59
- return ( path : NodePath < T > ) => {
60
- if (
61
- path . node . type === "ArrowFunctionExpression" ||
62
- path . node . type === "FunctionExpression"
63
- ) {
64
- return (
65
- path . parentPath . node . type === "VariableDeclarator" &&
66
- path . parentPath . node . id . type === "Identifier" &&
67
- predicate ( path . parentPath . node . id . name )
68
- ) ;
69
- } else if ( path . node . type === "FunctionDeclaration" ) {
70
- return predicate ( path . node . id ?. name ?? null ) ;
50
+ | BabelTypes . FunctionDeclaration
51
+ | BabelTypes . ObjectMethod ;
52
+
53
+ /**
54
+ * Simple "best effort" to get the base name of a file path. Not fool proof but
55
+ * works in browsers and servers. Good enough for our purposes.
56
+ */
57
+ function basename ( filename : string | undefined ) : string | undefined {
58
+ return filename ?. split ( / [ \\ / ] / ) . pop ( ) ;
59
+ }
60
+
61
+ const DefaultExportSymbol = Symbol ( "DefaultExportSymbol" ) ;
62
+
63
+ /**
64
+ * If the function node has a name (i.e. is a function declaration with a
65
+ * name), return that. Else return null.
66
+ */
67
+ function getFunctionNodeName ( path : NodePath < FunctionLike > ) : string | null {
68
+ if ( path . node . type === "FunctionDeclaration" && path . node . id ) {
69
+ return path . node . id . name ;
70
+ } else if ( path . node . type === "ObjectMethod" ) {
71
+ if ( path . node . key . type === "Identifier" ) {
72
+ return path . node . key . name ;
73
+ } else if ( path . node . key . type === "StringLiteral" ) {
74
+ return path . node . key . value ;
75
+ }
76
+ }
77
+
78
+ return null ;
79
+ }
80
+
81
+ /**
82
+ * Given a function path's parent path, determine the "name" associated with the
83
+ * function. If the function is an inline default export (e.g. `export default
84
+ * () => {}`), returns a symbol indicating it is a default export. If the
85
+ * function is an anonymous function wrapped in higher order functions (e.g.
86
+ * memo(() => {})) we'll climb through the higher order functions to find the
87
+ * name of the variable that the function is assigned to, if any. Other cases
88
+ * handled too (see implementation). Else returns null.
89
+ */
90
+ function getFunctionNameFromParent (
91
+ parentPath : NodePath < BabelTypes . Node >
92
+ ) : string | null | typeof DefaultExportSymbol {
93
+ if (
94
+ parentPath . node . type === "VariableDeclarator" &&
95
+ parentPath . node . id . type === "Identifier"
96
+ ) {
97
+ return parentPath . node . id . name ;
98
+ } else if ( parentPath . node . type === "AssignmentExpression" ) {
99
+ const left = parentPath . node . left ;
100
+ if ( left . type === "Identifier" ) {
101
+ return left . name ;
102
+ } else if ( left . type === "MemberExpression" ) {
103
+ let property = left . property ;
104
+ while ( property . type === "MemberExpression" ) {
105
+ property = property . property ;
106
+ }
107
+
108
+ if ( property . type === "Identifier" ) {
109
+ return property . name ;
110
+ } else if ( property . type === "StringLiteral" ) {
111
+ return property . value ;
112
+ }
113
+
114
+ return null ;
71
115
} else {
72
- return false ;
116
+ return null ;
73
117
}
74
- } ;
118
+ } else if ( parentPath . node . type === "ExportDefaultDeclaration" ) {
119
+ return DefaultExportSymbol ;
120
+ } else if (
121
+ parentPath . node . type === "CallExpression" &&
122
+ parentPath . parentPath != null
123
+ ) {
124
+ // If our parent is a Call Expression, then this function expression is
125
+ // wrapped in some higher order functions. Recurse through the higher order
126
+ // functions to determine if this expression is assigned to a name we can
127
+ // use as the function name
128
+ return getFunctionNameFromParent ( parentPath . parentPath ) ;
129
+ } else {
130
+ return null ;
131
+ }
132
+ }
133
+
134
+ /* Determine the name of a function */
135
+ function getFunctionName (
136
+ path : NodePath < FunctionLike >
137
+ ) : string | typeof DefaultExportSymbol | null {
138
+ let nodeName = getFunctionNodeName ( path ) ;
139
+ if ( nodeName ) {
140
+ return nodeName ;
141
+ }
142
+
143
+ return getFunctionNameFromParent ( path . parentPath ) ;
144
+ }
145
+
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 ;
75
156
}
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
+ }
76
166
77
- const fnNameStartsWithCapital = testFunctionName (
78
- name => name ?. match ( / ^ [ A - Z ] / ) !== null
79
- ) ;
80
- const fnNameStartsWithUse = testFunctionName (
81
- name => name ?. match ( / ^ u s e [ A - Z ] / ) !== null
82
- ) ;
167
+ return name . match ( / ^ u s e [ A - Z ] / ) != null ;
168
+ }
83
169
84
170
function hasLeadingComment ( path : NodePath , comment : RegExp ) : boolean {
85
171
const comments = path . node . leadingComments ;
@@ -101,9 +187,12 @@ function isOptedIntoSignalTracking(path: NodePath | null): boolean {
101
187
case "ArrowFunctionExpression" :
102
188
case "FunctionExpression" :
103
189
case "FunctionDeclaration" :
190
+ case "ObjectMethod" :
191
+ case "ObjectExpression" :
104
192
case "VariableDeclarator" :
105
193
case "VariableDeclaration" :
106
194
case "AssignmentExpression" :
195
+ case "CallExpression" :
107
196
return (
108
197
hasLeadingOptInComment ( path ) ||
109
198
isOptedIntoSignalTracking ( path . parentPath )
@@ -125,9 +214,12 @@ function isOptedOutOfSignalTracking(path: NodePath | null): boolean {
125
214
case "ArrowFunctionExpression" :
126
215
case "FunctionExpression" :
127
216
case "FunctionDeclaration" :
217
+ case "ObjectMethod" :
218
+ case "ObjectExpression" :
128
219
case "VariableDeclarator" :
129
220
case "VariableDeclaration" :
130
221
case "AssignmentExpression" :
222
+ case "CallExpression" :
131
223
return (
132
224
hasLeadingOptOutComment ( path ) ||
133
225
isOptedOutOfSignalTracking ( path . parentPath )
@@ -142,19 +234,26 @@ function isOptedOutOfSignalTracking(path: NodePath | null): boolean {
142
234
}
143
235
}
144
236
145
- function isComponentFunction ( path : NodePath < FunctionLike > ) : boolean {
237
+ function isComponentFunction (
238
+ path : NodePath < FunctionLike > ,
239
+ filename : string | undefined
240
+ ) : boolean {
146
241
return (
147
- fnNameStartsWithCapital ( path ) && // Function name indicates it's a component
148
- getData ( path . scope , containsJSX ) === true // Function contains JSX
242
+ getData ( path . scope , containsJSX ) === true && // Function contains JSX
243
+ fnNameStartsWithCapital ( path , filename ) // Function name indicates it's a component
149
244
) ;
150
245
}
151
246
152
- function isCustomHook ( path : NodePath < FunctionLike > ) : boolean {
153
- return fnNameStartsWithUse ( path ) ; // Function name indicates it's a hook
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
154
252
}
155
253
156
254
function shouldTransform (
157
255
path : NodePath < FunctionLike > ,
256
+ filename : string | undefined ,
158
257
options : PluginOptions
159
258
) : boolean {
160
259
if ( getData ( path , alreadyTransformed ) === true ) return false ;
@@ -165,14 +264,14 @@ function shouldTransform(
165
264
if ( isOptedIntoSignalTracking ( path ) ) return true ;
166
265
167
266
if ( options . mode === "all" ) {
168
- return isComponentFunction ( path ) ;
267
+ return isComponentFunction ( path , filename ) ;
169
268
}
170
269
171
270
if ( options . mode == null || options . mode === "auto" ) {
172
271
return (
173
- ( isComponentFunction ( path ) || isCustomHook ( path ) ) &&
174
- getData ( path . scope , maybeUsesSignal ) === true
175
- ) ; // Function appears to use signals;
272
+ getData ( path . scope , maybeUsesSignal ) === true && // Function appears to use signals;
273
+ ( isComponentFunction ( path , filename ) || isCustomHook ( path , filename ) )
274
+ ) ;
176
275
}
177
276
178
277
return false ;
@@ -242,10 +341,11 @@ function transformFunction(
242
341
t : typeof BabelTypes ,
243
342
options : PluginOptions ,
244
343
path : NodePath < FunctionLike > ,
344
+ filename : string | undefined ,
245
345
state : PluginPass
246
346
) {
247
347
let newFunction : FunctionLike ;
248
- if ( isCustomHook ( path ) || options . experimental ?. noTryFinally ) {
348
+ if ( isCustomHook ( path , filename ) || options . experimental ?. noTryFinally ) {
249
349
// For custom hooks, we don't need to wrap the function body in a
250
350
// try/finally block because later code in the function's render body could
251
351
// read signals and we want to track and associate those signals with this
@@ -369,24 +469,32 @@ export default function signalsTransform(
369
469
// seeing a function would probably be faster than running an entire
370
470
// babel pass with plugins on components twice.
371
471
exit ( path , state ) {
372
- if ( shouldTransform ( path , options ) ) {
373
- transformFunction ( t , options , path , state ) ;
472
+ if ( shouldTransform ( path , this . filename , options ) ) {
473
+ transformFunction ( t , options , path , this . filename , state ) ;
374
474
}
375
475
} ,
376
476
} ,
377
477
378
478
FunctionExpression : {
379
479
exit ( path , state ) {
380
- if ( shouldTransform ( path , options ) ) {
381
- transformFunction ( t , options , path , state ) ;
480
+ if ( shouldTransform ( path , this . filename , options ) ) {
481
+ transformFunction ( t , options , path , this . filename , state ) ;
382
482
}
383
483
} ,
384
484
} ,
385
485
386
486
FunctionDeclaration : {
387
487
exit ( path , state ) {
388
- if ( shouldTransform ( path , options ) ) {
389
- transformFunction ( t , options , 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 ) ;
390
498
}
391
499
} ,
392
500
} ,
0 commit comments