| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | 'use strict'; |
| 9 | |
| 10 | export default function (babel, opts = {}) { |
| 11 | if (typeof babel.env === 'function') { |
| 12 | // Only available in Babel 7. |
| 13 | const env = babel.env(); |
| 14 | if (env !== 'development' && !opts.skipEnvCheck) { |
| 15 | throw new Error( |
| 16 | 'React Refresh Babel transform should only be enabled in development environment. ' + |
| 17 | 'Instead, the environment is: "' + |
| 18 | env + |
| 19 | '". If you want to override this check, pass {skipEnvCheck: true} as plugin options.', |
| 20 | ); |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | const {types: t} = babel; |
| 25 | const refreshReg = t.identifier(opts.refreshReg || '$RefreshReg$'); |
| 26 | const refreshSig = t.identifier(opts.refreshSig || '$RefreshSig$'); |
| 27 | |
| 28 | const registrationsByProgramPath = new Map(); |
| 29 | function createRegistration(programPath, persistentID) { |
| 30 | const handle = programPath.scope.generateUidIdentifier('c'); |
| 31 | if (!registrationsByProgramPath.has(programPath)) { |
| 32 | registrationsByProgramPath.set(programPath, []); |
| 33 | } |
| 34 | const registrations = registrationsByProgramPath.get(programPath); |
| 35 | registrations.push({ |
| 36 | handle, |
| 37 | persistentID, |
| 38 | }); |
| 39 | return handle; |
| 40 | } |
| 41 | |
| 42 | function isComponentishName(name) { |
| 43 | return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z'; |
| 44 | } |
| 45 | |
| 46 | function findInnerComponents(inferredName, path, callback) { |
| 47 | const node = path.node; |
| 48 | switch (node.type) { |
| 49 | case 'Identifier': { |
| 50 | if (!isComponentishName(node.name)) { |
| 51 | return false; |
| 52 | } |
| 53 | // export default hoc(Foo) |
| 54 | // const X = hoc(Foo) |
| 55 | callback(inferredName, node, null); |
| 56 | return true; |
| 57 | } |
| 58 | case 'FunctionDeclaration': { |
| 59 | // function Foo() {} |
| 60 | // export function Foo() {} |
| 61 | // export default function Foo() {} |
| 62 | callback(inferredName, node.id, null); |
| 63 | return true; |
| 64 | } |
| 65 | case 'ArrowFunctionExpression': { |
| 66 | if (node.body.type === 'ArrowFunctionExpression') { |
| 67 | return false; |
| 68 | } |
| 69 | // let Foo = () => {} |
| 70 | // export default hoc1(hoc2(() => {})) |
| 71 | callback(inferredName, node, path); |
| 72 | return true; |
| 73 | } |
| 74 | case 'FunctionExpression': { |
| 75 | // let Foo = function() {} |
| 76 | // const Foo = hoc1(forwardRef(function renderFoo() {})) |
| 77 | // export default memo(function() {}) |
| 78 | callback(inferredName, node, path); |
| 79 | return true; |
| 80 | } |
| 81 | case 'CallExpression': { |
| 82 | const argsPath = path.get('arguments'); |
| 83 | if (argsPath === undefined || argsPath.length === 0) { |
| 84 | return false; |
| 85 | } |
| 86 | const calleePath = path.get('callee'); |
| 87 | switch (calleePath.node.type) { |
| 88 | case 'MemberExpression': |
| 89 | case 'Identifier': { |
| 90 | const calleeSource = calleePath.getSource(); |
| 91 | const firstArgPath = argsPath[0]; |
| 92 | const innerName = inferredName + '$' + calleeSource; |
| 93 | const foundInside = findInnerComponents( |
| 94 | innerName, |
| 95 | firstArgPath, |
| 96 | callback, |
| 97 | ); |
| 98 | if (!foundInside) { |
| 99 | return false; |
| 100 | } |
| 101 | // const Foo = hoc1(hoc2(() => {})) |
| 102 | // export default memo(React.forwardRef(function() {})) |
| 103 | callback(inferredName, node, path); |
| 104 | return true; |
| 105 | } |
| 106 | default: { |
| 107 | return false; |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | case 'VariableDeclarator': { |
| 112 | const init = node.init; |
| 113 | if (init === null) { |
| 114 | return false; |
| 115 | } |
| 116 | const name = node.id.name; |
| 117 | if (!isComponentishName(name)) { |
| 118 | return false; |
| 119 | } |
| 120 | switch (init.type) { |
| 121 | case 'ArrowFunctionExpression': |
| 122 | case 'FunctionExpression': |
| 123 | // Likely component definitions. |
| 124 | break; |
| 125 | case 'CallExpression': { |
| 126 | // Maybe a HOC. |
| 127 | // Try to determine if this is some form of import. |
| 128 | const callee = init.callee; |
| 129 | const calleeType = callee.type; |
| 130 | if (calleeType === 'Import') { |
| 131 | return false; |
| 132 | } else if (calleeType === 'Identifier') { |
| 133 | if (callee.name.indexOf('require') === 0) { |
| 134 | return false; |
| 135 | } else if (callee.name.indexOf('import') === 0) { |
| 136 | return false; |
| 137 | } |
| 138 | // Neither require nor import. Might be a HOC. |
| 139 | // Pass through. |
| 140 | } else if (calleeType === 'MemberExpression') { |
| 141 | // Could be something like React.forwardRef(...) |
| 142 | // Pass through. |
| 143 | } |
| 144 | break; |
| 145 | } |
| 146 | case 'TaggedTemplateExpression': |
| 147 | // Maybe something like styled.div`...` |
| 148 | break; |
| 149 | default: |
| 150 | return false; |
| 151 | } |
| 152 | const initPath = path.get('init'); |
| 153 | const foundInside = findInnerComponents( |
| 154 | inferredName, |
| 155 | initPath, |
| 156 | callback, |
| 157 | ); |
| 158 | if (foundInside) { |
| 159 | return true; |
| 160 | } |
| 161 | // See if this identifier is used in JSX. Then it's a component. |
| 162 | const binding = path.scope.getBinding(name); |
| 163 | if (binding === undefined) { |
| 164 | return; |
| 165 | } |
| 166 | let isLikelyUsedAsType = false; |
| 167 | const referencePaths = binding.referencePaths; |
| 168 | for (let i = 0; i < referencePaths.length; i++) { |
| 169 | const ref = referencePaths[i]; |
| 170 | if ( |
| 171 | ref.node && |
| 172 | ref.node.type !== 'JSXIdentifier' && |
| 173 | ref.node.type !== 'Identifier' |
| 174 | ) { |
| 175 | continue; |
| 176 | } |
| 177 | const refParent = ref.parent; |
| 178 | if (refParent.type === 'JSXOpeningElement') { |
| 179 | isLikelyUsedAsType = true; |
| 180 | } else if (refParent.type === 'CallExpression') { |
| 181 | const callee = refParent.callee; |
| 182 | let fnName; |
| 183 | switch (callee.type) { |
| 184 | case 'Identifier': |
| 185 | fnName = callee.name; |
| 186 | break; |
| 187 | case 'MemberExpression': |
| 188 | fnName = callee.property.name; |
| 189 | break; |
| 190 | } |
| 191 | switch (fnName) { |
| 192 | case 'createElement': |
| 193 | case 'jsx': |
| 194 | case 'jsxDEV': |
| 195 | case 'jsxs': |
| 196 | isLikelyUsedAsType = true; |
| 197 | break; |
| 198 | } |
| 199 | } |
| 200 | if (isLikelyUsedAsType) { |
| 201 | // const X = ... + later <X /> |
| 202 | callback(inferredName, init, initPath); |
| 203 | return true; |
| 204 | } |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | function isBuiltinHook(hookName) { |
| 212 | switch (hookName) { |
| 213 | case 'useState': |
| 214 | case 'React.useState': |
| 215 | case 'useReducer': |
| 216 | case 'React.useReducer': |
| 217 | case 'useEffect': |
| 218 | case 'React.useEffect': |
| 219 | case 'useLayoutEffect': |
| 220 | case 'React.useLayoutEffect': |
| 221 | case 'useMemo': |
| 222 | case 'React.useMemo': |
| 223 | case 'useCallback': |
| 224 | case 'React.useCallback': |
| 225 | case 'useRef': |
| 226 | case 'React.useRef': |
| 227 | case 'useContext': |
| 228 | case 'React.useContext': |
| 229 | case 'useImperativeHandle': |
| 230 | case 'React.useImperativeHandle': |
| 231 | case 'useDebugValue': |
| 232 | case 'React.useDebugValue': |
| 233 | case 'useId': |
| 234 | case 'React.useId': |
| 235 | case 'useDeferredValue': |
| 236 | case 'React.useDeferredValue': |
| 237 | case 'useTransition': |
| 238 | case 'React.useTransition': |
| 239 | case 'useInsertionEffect': |
| 240 | case 'React.useInsertionEffect': |
| 241 | case 'useSyncExternalStore': |
| 242 | case 'React.useSyncExternalStore': |
| 243 | case 'useFormStatus': |
| 244 | case 'React.useFormStatus': |
| 245 | case 'useFormState': |
| 246 | case 'React.useFormState': |
| 247 | case 'useActionState': |
| 248 | case 'React.useActionState': |
| 249 | case 'useOptimistic': |
| 250 | case 'React.useOptimistic': |
| 251 | return true; |
| 252 | default: |
| 253 | return false; |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | function getHookCallsSignature(functionNode) { |
| 258 | const fnHookCalls = hookCalls.get(functionNode); |
| 259 | if (fnHookCalls === undefined) { |
| 260 | return null; |
| 261 | } |
| 262 | return { |
| 263 | key: fnHookCalls.map(call => call.name + '{' + call.key + '}').join('\n'), |
| 264 | customHooks: fnHookCalls |
| 265 | .filter(call => !isBuiltinHook(call.name)) |
| 266 | .map(call => t.cloneDeep(call.callee)), |
| 267 | }; |
| 268 | } |
| 269 | |
| 270 | const hasForceResetCommentByFile = new WeakMap(); |
| 271 | |
| 272 | // We let user do /* @refresh reset */ to reset state in the whole file. |
| 273 | function hasForceResetComment(path) { |
| 274 | const file = path.hub.file; |
| 275 | let hasForceReset = hasForceResetCommentByFile.get(file); |
| 276 | if (hasForceReset !== undefined) { |
| 277 | return hasForceReset; |
| 278 | } |
| 279 | |
| 280 | hasForceReset = false; |
| 281 | const comments = file.ast.comments; |
| 282 | for (let i = 0; i < comments.length; i++) { |
| 283 | const cmt = comments[i]; |
| 284 | if (cmt.value.indexOf('@refresh reset') !== -1) { |
| 285 | hasForceReset = true; |
| 286 | break; |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | hasForceResetCommentByFile.set(file, hasForceReset); |
| 291 | return hasForceReset; |
| 292 | } |
| 293 | |
| 294 | function createArgumentsForSignature(node, signature, scope) { |
| 295 | const {key, customHooks} = signature; |
| 296 | |
| 297 | let forceReset = hasForceResetComment(scope.path); |
| 298 | const customHooksInScope = []; |
| 299 | customHooks.forEach(callee => { |
| 300 | // Check if a corresponding binding exists where we emit the signature. |
| 301 | let bindingName; |
| 302 | switch (callee.type) { |
| 303 | case 'MemberExpression': |
| 304 | if (callee.object.type === 'Identifier') { |
| 305 | bindingName = callee.object.name; |
| 306 | } |
| 307 | break; |
| 308 | case 'Identifier': |
| 309 | bindingName = callee.name; |
| 310 | break; |
| 311 | } |
| 312 | if (scope.hasBinding(bindingName)) { |
| 313 | customHooksInScope.push(callee); |
| 314 | } else { |
| 315 | // We don't have anything to put in the array because Hook is out of scope. |
| 316 | // Since it could potentially have been edited, remount the component. |
| 317 | forceReset = true; |
| 318 | } |
| 319 | }); |
| 320 | |
| 321 | let finalKey = key; |
| 322 | if (typeof require === 'function' && !opts.emitFullSignatures) { |
| 323 | // Prefer to hash when we can (e.g. outside of ASTExplorer). |
| 324 | // This makes it deterministically compact, even if there's |
| 325 | // e.g. a useState initializer with some code inside. |
| 326 | // We also need it for www that has transforms like cx() |
| 327 | // that don't understand if something is part of a string. |
| 328 | finalKey = require('crypto') |
| 329 | .createHash('sha1') |
| 330 | .update(key) |
| 331 | .digest('base64'); |
| 332 | } |
| 333 | |
| 334 | const args = [node, t.stringLiteral(finalKey)]; |
| 335 | if (forceReset || customHooksInScope.length > 0) { |
| 336 | args.push(t.booleanLiteral(forceReset)); |
| 337 | } |
| 338 | if (customHooksInScope.length > 0) { |
| 339 | args.push( |
| 340 | // TODO: We could use an arrow here to be more compact. |
| 341 | // However, don't do it until AMA can run them natively. |
| 342 | t.functionExpression( |
| 343 | null, |
| 344 | [], |
| 345 | t.blockStatement([ |
| 346 | t.returnStatement(t.arrayExpression(customHooksInScope)), |
| 347 | ]), |
| 348 | ), |
| 349 | ); |
| 350 | } |
| 351 | return args; |
| 352 | } |
| 353 | |
| 354 | function findHOCCallPathsAbove(path) { |
| 355 | const calls = []; |
| 356 | while (true) { |
| 357 | if (!path) { |
| 358 | return calls; |
| 359 | } |
| 360 | const parentPath = path.parentPath; |
| 361 | if (!parentPath) { |
| 362 | return calls; |
| 363 | } |
| 364 | if ( |
| 365 | // hoc(_c = function() { }) |
| 366 | parentPath.node.type === 'AssignmentExpression' && |
| 367 | path.node === parentPath.node.right |
| 368 | ) { |
| 369 | // Ignore registrations. |
| 370 | path = parentPath; |
| 371 | continue; |
| 372 | } |
| 373 | if ( |
| 374 | // hoc1(hoc2(...)) |
| 375 | parentPath.node.type === 'CallExpression' && |
| 376 | path.node !== parentPath.node.callee |
| 377 | ) { |
| 378 | calls.push(parentPath); |
| 379 | path = parentPath; |
| 380 | continue; |
| 381 | } |
| 382 | return calls; // Stop at other types. |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | const seenForRegistration = new WeakSet(); |
| 387 | const seenForSignature = new WeakSet(); |
| 388 | const seenForOutro = new WeakSet(); |
| 389 | |
| 390 | const hookCalls = new WeakMap(); |
| 391 | const HookCallsVisitor = { |
| 392 | CallExpression(path) { |
| 393 | const node = path.node; |
| 394 | const callee = node.callee; |
| 395 | |
| 396 | // Note: this visitor MUST NOT mutate the tree in any way. |
| 397 | // It runs early in a separate traversal and should be very fast. |
| 398 | |
| 399 | let name = null; |
| 400 | switch (callee.type) { |
| 401 | case 'Identifier': |
| 402 | name = callee.name; |
| 403 | break; |
| 404 | case 'MemberExpression': |
| 405 | name = callee.property.name; |
| 406 | break; |
| 407 | } |
| 408 | if (name === null || !/^use[A-Z]/.test(name)) { |
| 409 | return; |
| 410 | } |
| 411 | const fnScope = path.scope.getFunctionParent(); |
| 412 | if (fnScope === null) { |
| 413 | return; |
| 414 | } |
| 415 | |
| 416 | // This is a Hook call. Record it. |
| 417 | const fnNode = fnScope.block; |
| 418 | if (!hookCalls.has(fnNode)) { |
| 419 | hookCalls.set(fnNode, []); |
| 420 | } |
| 421 | const hookCallsForFn = hookCalls.get(fnNode); |
| 422 | let key = ''; |
| 423 | if (path.parent.type === 'VariableDeclarator') { |
| 424 | // TODO: if there is no LHS, consider some other heuristic. |
| 425 | key = path.parentPath.get('id').getSource(); |
| 426 | } |
| 427 | |
| 428 | // Some built-in Hooks reset on edits to arguments. |
| 429 | const args = path.get('arguments'); |
| 430 | if (name === 'useState' && args.length > 0) { |
| 431 | // useState second argument is initial state. |
| 432 | key += '(' + args[0].getSource() + ')'; |
| 433 | } else if (name === 'useReducer' && args.length > 1) { |
| 434 | // useReducer second argument is initial state. |
| 435 | key += '(' + args[1].getSource() + ')'; |
| 436 | } |
| 437 | |
| 438 | hookCallsForFn.push({ |
| 439 | callee: path.node.callee, |
| 440 | name, |
| 441 | key, |
| 442 | }); |
| 443 | }, |
| 444 | }; |
| 445 | |
| 446 | return { |
| 447 | visitor: { |
| 448 | ExportDefaultDeclaration(path) { |
| 449 | const node = path.node; |
| 450 | const decl = node.declaration; |
| 451 | const declPath = path.get('declaration'); |
| 452 | if (decl.type !== 'CallExpression') { |
| 453 | // For now, we only support possible HOC calls here. |
| 454 | // Named function declarations are handled in FunctionDeclaration. |
| 455 | // Anonymous direct exports like export default function() {} |
| 456 | // are currently ignored. |
| 457 | return; |
| 458 | } |
| 459 | |
| 460 | // Make sure we're not mutating the same tree twice. |
| 461 | // This can happen if another Babel plugin replaces parents. |
| 462 | if (seenForRegistration.has(node)) { |
| 463 | return; |
| 464 | } |
| 465 | seenForRegistration.add(node); |
| 466 | // Don't mutate the tree above this point. |
| 467 | |
| 468 | // This code path handles nested cases like: |
| 469 | // export default memo(() => {}) |
| 470 | // In those cases it is more plausible people will omit names |
| 471 | // so they're worth handling despite possible false positives. |
| 472 | // More importantly, it handles the named case: |
| 473 | // export default memo(function Named() {}) |
| 474 | const inferredName = '%default%'; |
| 475 | const programPath = path.parentPath; |
| 476 | findInnerComponents( |
| 477 | inferredName, |
| 478 | declPath, |
| 479 | (persistentID, targetExpr, targetPath) => { |
| 480 | if (targetPath === null) { |
| 481 | // For case like: |
| 482 | // export default hoc(Foo) |
| 483 | // we don't want to wrap Foo inside the call. |
| 484 | // Instead we assume it's registered at definition. |
| 485 | return; |
| 486 | } |
| 487 | const handle = createRegistration(programPath, persistentID); |
| 488 | targetPath.replaceWith( |
| 489 | t.assignmentExpression('=', handle, targetExpr), |
| 490 | ); |
| 491 | }, |
| 492 | ); |
| 493 | }, |
| 494 | FunctionDeclaration: { |
| 495 | enter(path) { |
| 496 | const node = path.node; |
| 497 | let programPath; |
| 498 | let insertAfterPath; |
| 499 | let modulePrefix = ''; |
| 500 | switch (path.parent.type) { |
| 501 | case 'Program': |
| 502 | insertAfterPath = path; |
| 503 | programPath = path.parentPath; |
| 504 | break; |
| 505 | case 'TSModuleBlock': |
| 506 | insertAfterPath = path; |
| 507 | programPath = insertAfterPath.parentPath.parentPath; |
| 508 | break; |
| 509 | case 'ExportNamedDeclaration': |
| 510 | insertAfterPath = path.parentPath; |
| 511 | programPath = insertAfterPath.parentPath; |
| 512 | break; |
| 513 | case 'ExportDefaultDeclaration': |
| 514 | insertAfterPath = path.parentPath; |
| 515 | programPath = insertAfterPath.parentPath; |
| 516 | break; |
| 517 | default: |
| 518 | return; |
| 519 | } |
| 520 | |
| 521 | // These types can be nested in typescript namespace |
| 522 | // We need to find the export chain |
| 523 | // Or return if it stays local |
| 524 | if ( |
| 525 | path.parent.type === 'TSModuleBlock' || |
| 526 | path.parent.type === 'ExportNamedDeclaration' |
| 527 | ) { |
| 528 | while (programPath.type !== 'Program') { |
| 529 | if (programPath.type === 'TSModuleDeclaration') { |
| 530 | if ( |
| 531 | programPath.parentPath.type !== 'Program' && |
| 532 | programPath.parentPath.type !== 'ExportNamedDeclaration' |
| 533 | ) { |
| 534 | return; |
| 535 | } |
| 536 | modulePrefix = programPath.node.id.name + '$' + modulePrefix; |
| 537 | } |
| 538 | programPath = programPath.parentPath; |
| 539 | } |
| 540 | } |
| 541 | |
| 542 | const id = node.id; |
| 543 | if (id === null) { |
| 544 | // We don't currently handle anonymous default exports. |
| 545 | return; |
| 546 | } |
| 547 | const inferredName = id.name; |
| 548 | if (!isComponentishName(inferredName)) { |
| 549 | return; |
| 550 | } |
| 551 | |
| 552 | // Make sure we're not mutating the same tree twice. |
| 553 | // This can happen if another Babel plugin replaces parents. |
| 554 | if (seenForRegistration.has(node)) { |
| 555 | return; |
| 556 | } |
| 557 | seenForRegistration.add(node); |
| 558 | // Don't mutate the tree above this point. |
| 559 | |
| 560 | const innerName = modulePrefix + inferredName; |
| 561 | // export function Named() {} |
| 562 | // function Named() {} |
| 563 | findInnerComponents(innerName, path, (persistentID, targetExpr) => { |
| 564 | const handle = createRegistration(programPath, persistentID); |
| 565 | insertAfterPath.insertAfter( |
| 566 | t.expressionStatement( |
| 567 | t.assignmentExpression('=', handle, targetExpr), |
| 568 | ), |
| 569 | ); |
| 570 | }); |
| 571 | }, |
| 572 | exit(path) { |
| 573 | const node = path.node; |
| 574 | const id = node.id; |
| 575 | if (id === null) { |
| 576 | return; |
| 577 | } |
| 578 | const signature = getHookCallsSignature(node); |
| 579 | if (signature === null) { |
| 580 | return; |
| 581 | } |
| 582 | |
| 583 | // Make sure we're not mutating the same tree twice. |
| 584 | // This can happen if another Babel plugin replaces parents. |
| 585 | if (seenForSignature.has(node)) { |
| 586 | return; |
| 587 | } |
| 588 | seenForSignature.add(node); |
| 589 | // Don't mutate the tree above this point. |
| 590 | |
| 591 | const sigCallID = path.scope.generateUidIdentifier('_s'); |
| 592 | path.scope.parent.push({ |
| 593 | id: sigCallID, |
| 594 | init: t.callExpression(refreshSig, []), |
| 595 | }); |
| 596 | |
| 597 | // The signature call is split in two parts. One part is called inside the function. |
| 598 | // This is used to signal when first render happens. |
| 599 | path |
| 600 | .get('body') |
| 601 | .unshiftContainer( |
| 602 | 'body', |
| 603 | t.expressionStatement(t.callExpression(sigCallID, [])), |
| 604 | ); |
| 605 | |
| 606 | // The second call is around the function itself. |
| 607 | // This is used to associate a type with a signature. |
| 608 | |
| 609 | // Unlike with $RefreshReg$, this needs to work for nested |
| 610 | // declarations too. So we need to search for a path where |
| 611 | // we can insert a statement rather than hard coding it. |
| 612 | let insertAfterPath = null; |
| 613 | path.find(p => { |
| 614 | if (p.parentPath.isBlock()) { |
| 615 | insertAfterPath = p; |
| 616 | return true; |
| 617 | } |
| 618 | }); |
| 619 | if (insertAfterPath === null) { |
| 620 | return; |
| 621 | } |
| 622 | |
| 623 | insertAfterPath.insertAfter( |
| 624 | t.expressionStatement( |
| 625 | t.callExpression( |
| 626 | sigCallID, |
| 627 | createArgumentsForSignature( |
| 628 | id, |
| 629 | signature, |
| 630 | insertAfterPath.scope, |
| 631 | ), |
| 632 | ), |
| 633 | ), |
| 634 | ); |
| 635 | }, |
| 636 | }, |
| 637 | 'ArrowFunctionExpression|FunctionExpression': { |
| 638 | exit(path) { |
| 639 | const node = path.node; |
| 640 | const signature = getHookCallsSignature(node); |
| 641 | if (signature === null) { |
| 642 | return; |
| 643 | } |
| 644 | |
| 645 | // Make sure we're not mutating the same tree twice. |
| 646 | // This can happen if another Babel plugin replaces parents. |
| 647 | if (seenForSignature.has(node)) { |
| 648 | return; |
| 649 | } |
| 650 | seenForSignature.add(node); |
| 651 | // Don't mutate the tree above this point. |
| 652 | |
| 653 | const sigCallID = path.scope.generateUidIdentifier('_s'); |
| 654 | path.scope.parent.push({ |
| 655 | id: sigCallID, |
| 656 | init: t.callExpression(refreshSig, []), |
| 657 | }); |
| 658 | |
| 659 | // The signature call is split in two parts. One part is called inside the function. |
| 660 | // This is used to signal when first render happens. |
| 661 | if (path.node.body.type !== 'BlockStatement') { |
| 662 | path.node.body = t.blockStatement([ |
| 663 | t.returnStatement(path.node.body), |
| 664 | ]); |
| 665 | } |
| 666 | path |
| 667 | .get('body') |
| 668 | .unshiftContainer( |
| 669 | 'body', |
| 670 | t.expressionStatement(t.callExpression(sigCallID, [])), |
| 671 | ); |
| 672 | |
| 673 | // The second call is around the function itself. |
| 674 | // This is used to associate a type with a signature. |
| 675 | |
| 676 | if (path.parent.type === 'VariableDeclarator') { |
| 677 | let insertAfterPath = null; |
| 678 | path.find(p => { |
| 679 | if (p.parentPath.isBlock()) { |
| 680 | insertAfterPath = p; |
| 681 | return true; |
| 682 | } |
| 683 | }); |
| 684 | if (insertAfterPath === null) { |
| 685 | return; |
| 686 | } |
| 687 | // Special case when a function would get an inferred name: |
| 688 | // let Foo = () => {} |
| 689 | // let Foo = function() {} |
| 690 | // We'll add signature it on next line so that |
| 691 | // we don't mess up the inferred 'Foo' function name. |
| 692 | insertAfterPath.insertAfter( |
| 693 | t.expressionStatement( |
| 694 | t.callExpression( |
| 695 | sigCallID, |
| 696 | createArgumentsForSignature( |
| 697 | path.parent.id, |
| 698 | signature, |
| 699 | insertAfterPath.scope, |
| 700 | ), |
| 701 | ), |
| 702 | ), |
| 703 | ); |
| 704 | // Result: let Foo = () => {}; __signature(Foo, ...); |
| 705 | } else { |
| 706 | // let Foo = hoc(() => {}) |
| 707 | const paths = [path, ...findHOCCallPathsAbove(path)]; |
| 708 | paths.forEach(p => { |
| 709 | p.replaceWith( |
| 710 | t.callExpression( |
| 711 | sigCallID, |
| 712 | createArgumentsForSignature(p.node, signature, p.scope), |
| 713 | ), |
| 714 | ); |
| 715 | }); |
| 716 | // Result: let Foo = __signature(hoc(__signature(() => {}, ...)), ...) |
| 717 | } |
| 718 | }, |
| 719 | }, |
| 720 | VariableDeclaration(path) { |
| 721 | const node = path.node; |
| 722 | let programPath; |
| 723 | let insertAfterPath; |
| 724 | let modulePrefix = ''; |
| 725 | switch (path.parent.type) { |
| 726 | case 'Program': |
| 727 | insertAfterPath = path; |
| 728 | programPath = path.parentPath; |
| 729 | break; |
| 730 | case 'TSModuleBlock': |
| 731 | insertAfterPath = path; |
| 732 | programPath = insertAfterPath.parentPath.parentPath; |
| 733 | break; |
| 734 | case 'ExportNamedDeclaration': |
| 735 | insertAfterPath = path.parentPath; |
| 736 | programPath = insertAfterPath.parentPath; |
| 737 | break; |
| 738 | case 'ExportDefaultDeclaration': |
| 739 | insertAfterPath = path.parentPath; |
| 740 | programPath = insertAfterPath.parentPath; |
| 741 | break; |
| 742 | default: |
| 743 | return; |
| 744 | } |
| 745 | |
| 746 | // These types can be nested in typescript namespace |
| 747 | // We need to find the export chain |
| 748 | // Or return if it stays local |
| 749 | if ( |
| 750 | path.parent.type === 'TSModuleBlock' || |
| 751 | path.parent.type === 'ExportNamedDeclaration' |
| 752 | ) { |
| 753 | while (programPath.type !== 'Program') { |
| 754 | if (programPath.type === 'TSModuleDeclaration') { |
| 755 | if ( |
| 756 | programPath.parentPath.type !== 'Program' && |
| 757 | programPath.parentPath.type !== 'ExportNamedDeclaration' |
| 758 | ) { |
| 759 | return; |
| 760 | } |
| 761 | modulePrefix = programPath.node.id.name + '$' + modulePrefix; |
| 762 | } |
| 763 | programPath = programPath.parentPath; |
| 764 | } |
| 765 | } |
| 766 | |
| 767 | // Make sure we're not mutating the same tree twice. |
| 768 | // This can happen if another Babel plugin replaces parents. |
| 769 | if (seenForRegistration.has(node)) { |
| 770 | return; |
| 771 | } |
| 772 | seenForRegistration.add(node); |
| 773 | // Don't mutate the tree above this point. |
| 774 | |
| 775 | const declPaths = path.get('declarations'); |
| 776 | if (declPaths.length !== 1) { |
| 777 | return; |
| 778 | } |
| 779 | const declPath = declPaths[0]; |
| 780 | const inferredName = declPath.node.id.name; |
| 781 | const innerName = modulePrefix + inferredName; |
| 782 | findInnerComponents( |
| 783 | innerName, |
| 784 | declPath, |
| 785 | (persistentID, targetExpr, targetPath) => { |
| 786 | if (targetPath === null) { |
| 787 | // For case like: |
| 788 | // export const Something = hoc(Foo) |
| 789 | // we don't want to wrap Foo inside the call. |
| 790 | // Instead we assume it's registered at definition. |
| 791 | return; |
| 792 | } |
| 793 | const handle = createRegistration(programPath, persistentID); |
| 794 | if (targetPath.parent.type === 'VariableDeclarator') { |
| 795 | // Special case when a variable would get an inferred name: |
| 796 | // let Foo = () => {} |
| 797 | // let Foo = function() {} |
| 798 | // let Foo = styled.div``; |
| 799 | // We'll register it on next line so that |
| 800 | // we don't mess up the inferred 'Foo' function name. |
| 801 | // (eg: with @babel/plugin-transform-react-display-name or |
| 802 | // babel-plugin-styled-components) |
| 803 | insertAfterPath.insertAfter( |
| 804 | t.expressionStatement( |
| 805 | t.assignmentExpression('=', handle, declPath.node.id), |
| 806 | ), |
| 807 | ); |
| 808 | // Result: let Foo = () => {}; _c1 = Foo; |
| 809 | } else { |
| 810 | // let Foo = hoc(() => {}) |
| 811 | targetPath.replaceWith( |
| 812 | t.assignmentExpression('=', handle, targetExpr), |
| 813 | ); |
| 814 | // Result: let Foo = hoc(_c1 = () => {}) |
| 815 | } |
| 816 | }, |
| 817 | ); |
| 818 | }, |
| 819 | Program: { |
| 820 | enter(path) { |
| 821 | // This is a separate early visitor because we need to collect Hook calls |
| 822 | // and "const [foo, setFoo] = ..." signatures before the destructuring |
| 823 | // transform mangles them. This extra traversal is not ideal for perf, |
| 824 | // but it's the best we can do until we stop transpiling destructuring. |
| 825 | path.traverse(HookCallsVisitor); |
| 826 | }, |
| 827 | exit(path) { |
| 828 | const registrations = registrationsByProgramPath.get(path); |
| 829 | if (registrations === undefined) { |
| 830 | return; |
| 831 | } |
| 832 | |
| 833 | // Make sure we're not mutating the same tree twice. |
| 834 | // This can happen if another Babel plugin replaces parents. |
| 835 | const node = path.node; |
| 836 | if (seenForOutro.has(node)) { |
| 837 | return; |
| 838 | } |
| 839 | seenForOutro.add(node); |
| 840 | // Don't mutate the tree above this point. |
| 841 | |
| 842 | registrationsByProgramPath.delete(path); |
| 843 | const declarators = []; |
| 844 | path.pushContainer('body', t.variableDeclaration('var', declarators)); |
| 845 | registrations.forEach(({handle, persistentID}) => { |
| 846 | path.pushContainer( |
| 847 | 'body', |
| 848 | t.expressionStatement( |
| 849 | t.callExpression(refreshReg, [ |
| 850 | handle, |
| 851 | t.stringLiteral(persistentID), |
| 852 | ]), |
| 853 | ), |
| 854 | ); |
| 855 | declarators.push(t.variableDeclarator(handle)); |
| 856 | }); |
| 857 | }, |
| 858 | }, |
| 859 | }, |
| 860 | }; |
| 861 | } |