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. const babel = require('@babel/core');
    
  11. const {wrap} = require('jest-snapshot-serializer-raw');
    
  12. const freshPlugin = require('react-refresh/babel');
    
  13. 
    
  14. function transform(input, options = {}) {
    
  15.   return wrap(
    
  16.     babel.transform(input, {
    
  17.       babelrc: false,
    
  18.       configFile: false,
    
  19.       envName: options.envName,
    
  20.       plugins: [
    
  21.         '@babel/syntax-jsx',
    
  22.         '@babel/syntax-dynamic-import',
    
  23.         [
    
  24.           freshPlugin,
    
  25.           {
    
  26.             skipEnvCheck:
    
  27.               options.skipEnvCheck === undefined ? true : options.skipEnvCheck,
    
  28.             // To simplify debugging tests:
    
  29.             emitFullSignatures: true,
    
  30.             ...options.freshOptions,
    
  31.           },
    
  32.         ],
    
  33.         ...(options.plugins || []),
    
  34.       ],
    
  35.     }).code,
    
  36.   );
    
  37. }
    
  38. 
    
  39. describe('ReactFreshBabelPlugin', () => {
    
  40.   it('registers top-level function declarations', () => {
    
  41.     // Hello and Bar should be registered, handleClick shouldn't.
    
  42.     expect(
    
  43.       transform(`
    
  44.         function Hello() {
    
  45.           function handleClick() {}
    
  46.           return <h1 onClick={handleClick}>Hi</h1>;
    
  47.         }
    
  48. 
    
  49.         function Bar() {
    
  50.           return <Hello />;
    
  51.         }
    
  52.     `),
    
  53.     ).toMatchSnapshot();
    
  54.   });
    
  55. 
    
  56.   it('registers top-level exported function declarations', () => {
    
  57.     expect(
    
  58.       transform(`
    
  59.         export function Hello() {
    
  60.           function handleClick() {}
    
  61.           return <h1 onClick={handleClick}>Hi</h1>;
    
  62.         }
    
  63. 
    
  64.         export default function Bar() {
    
  65.           return <Hello />;
    
  66.         }
    
  67. 
    
  68.         function Baz() {
    
  69.           return <h1>OK</h1>;
    
  70.         }
    
  71. 
    
  72.         const NotAComp = 'hi';
    
  73.         export { Baz, NotAComp };
    
  74. 
    
  75.         export function sum() {}
    
  76.         export const Bad = 42;
    
  77.     `),
    
  78.     ).toMatchSnapshot();
    
  79.   });
    
  80. 
    
  81.   it('registers top-level exported named arrow functions', () => {
    
  82.     expect(
    
  83.       transform(`
    
  84.         export const Hello = () => {
    
  85.           function handleClick() {}
    
  86.           return <h1 onClick={handleClick}>Hi</h1>;
    
  87.         };
    
  88. 
    
  89.         export let Bar = (props) => <Hello />;
    
  90. 
    
  91.         export default () => {
    
  92.           // This one should be ignored.
    
  93.           // You should name your components.
    
  94.           return <Hello />;
    
  95.         };
    
  96.     `),
    
  97.     ).toMatchSnapshot();
    
  98.   });
    
  99. 
    
  100.   it('uses original function declaration if it get reassigned', () => {
    
  101.     // This should register the original version.
    
  102.     // TODO: in the future, we may *also* register the wrapped one.
    
  103.     expect(
    
  104.       transform(`
    
  105.         function Hello() {
    
  106.           return <h1>Hi</h1>;
    
  107.         }
    
  108.         Hello = connect(Hello);
    
  109.     `),
    
  110.     ).toMatchSnapshot();
    
  111.   });
    
  112. 
    
  113.   it('only registers pascal case functions', () => {
    
  114.     // Should not get registered.
    
  115.     expect(
    
  116.       transform(`
    
  117.         function hello() {
    
  118.           return 2 * 2;
    
  119.         }
    
  120.     `),
    
  121.     ).toMatchSnapshot();
    
  122.   });
    
  123. 
    
  124.   it('registers top-level variable declarations with function expressions', () => {
    
  125.     // Hello and Bar should be registered; handleClick, sum, Baz, and Qux shouldn't.
    
  126.     expect(
    
  127.       transform(`
    
  128.         let Hello = function() {
    
  129.           function handleClick() {}
    
  130.           return <h1 onClick={handleClick}>Hi</h1>;
    
  131.         };
    
  132.         const Bar = function Baz() {
    
  133.           return <Hello />;
    
  134.         };
    
  135.         function sum() {}
    
  136.         let Baz = 10;
    
  137.         var Qux;
    
  138.     `),
    
  139.     ).toMatchSnapshot();
    
  140.   });
    
  141. 
    
  142.   it('registers top-level variable declarations with arrow functions', () => {
    
  143.     // Hello, Bar, and Baz should be registered; handleClick and sum shouldn't.
    
  144.     expect(
    
  145.       transform(`
    
  146.         let Hello = () => {
    
  147.           const handleClick = () => {};
    
  148.           return <h1 onClick={handleClick}>Hi</h1>;
    
  149.         }
    
  150.         const Bar = () => {
    
  151.           return <Hello />;
    
  152.         };
    
  153.         var Baz = () => <div />;
    
  154.         var sum = () => {};
    
  155.     `),
    
  156.     ).toMatchSnapshot();
    
  157.   });
    
  158. 
    
  159.   it('ignores HOC definitions', () => {
    
  160.     // TODO: we might want to handle HOCs at usage site, however.
    
  161.     // TODO: it would be nice if we could always avoid registering
    
  162.     // a function that is known to return a function or other non-node.
    
  163.     expect(
    
  164.       transform(`
    
  165.         let connect = () => {
    
  166.           function Comp() {
    
  167.             const handleClick = () => {};
    
  168.             return <h1 onClick={handleClick}>Hi</h1>;
    
  169.           }
    
  170.           return Comp;
    
  171.         };
    
  172.         function withRouter() {
    
  173.           return function Child() {
    
  174.             const handleClick = () => {};
    
  175.             return <h1 onClick={handleClick}>Hi</h1>;
    
  176.           }
    
  177.         };
    
  178.     `),
    
  179.     ).toMatchSnapshot();
    
  180.   });
    
  181. 
    
  182.   it('ignores complex definitions', () => {
    
  183.     expect(
    
  184.       transform(`
    
  185.         let A = foo ? () => {
    
  186.           return <h1>Hi</h1>;
    
  187.         } : null
    
  188.         const B = (function Foo() {
    
  189.           return <h1>Hi</h1>;
    
  190.         })();
    
  191.         let C = () => () => {
    
  192.           return <h1>Hi</h1>;
    
  193.         };
    
  194.         let D = bar && (() => {
    
  195.           return <h1>Hi</h1>;
    
  196.         });
    
  197.     `),
    
  198.     ).toMatchSnapshot();
    
  199.   });
    
  200. 
    
  201.   it('ignores unnamed function declarations', () => {
    
  202.     expect(
    
  203.       transform(`
    
  204.         export default function() {}
    
  205.     `),
    
  206.     ).toMatchSnapshot();
    
  207.   });
    
  208. 
    
  209.   it('registers likely HOCs with inline functions', () => {
    
  210.     expect(
    
  211.       transform(`
    
  212.         const A = forwardRef(function() {
    
  213.           return <h1>Foo</h1>;
    
  214.         });
    
  215.         const B = memo(React.forwardRef(() => {
    
  216.           return <h1>Foo</h1>;
    
  217.         }));
    
  218.         export default React.memo(forwardRef((props, ref) => {
    
  219.           return <h1>Foo</h1>;
    
  220.         }));
    
  221.     `),
    
  222.     ).toMatchSnapshot();
    
  223.     expect(
    
  224.       transform(`
    
  225.         export default React.memo(forwardRef(function (props, ref) {
    
  226.           return <h1>Foo</h1>;
    
  227.         }));
    
  228.     `),
    
  229.     ).toMatchSnapshot();
    
  230.     expect(
    
  231.       transform(`
    
  232.         export default React.memo(forwardRef(function Named(props, ref) {
    
  233.           return <h1>Foo</h1>;
    
  234.         }));
    
  235.     `),
    
  236.     ).toMatchSnapshot();
    
  237.   });
    
  238. 
    
  239.   it('ignores higher-order functions that are not HOCs', () => {
    
  240.     expect(
    
  241.       transform(`
    
  242.         const throttledAlert = throttle(function() {
    
  243.           alert('Hi');
    
  244.         });
    
  245.         const TooComplex = (function() { return hello })(() => {});
    
  246.         if (cond) {
    
  247.           const Foo = thing(() => {});
    
  248.         }
    
  249.     `),
    
  250.     ).toMatchSnapshot();
    
  251.   });
    
  252. 
    
  253.   it('registers identifiers used in JSX at definition site', () => {
    
  254.     // When in doubt, register variables that were used in JSX.
    
  255.     // Foo, Header, and B get registered.
    
  256.     // A doesn't get registered because it's not declared locally.
    
  257.     // Alias doesn't get registered because its definition is just an identifier.
    
  258.     expect(
    
  259.       transform(`
    
  260.         import A from './A';
    
  261.         import Store from './Store';
    
  262. 
    
  263.         Store.subscribe();
    
  264. 
    
  265.         const Header = styled.div\`color: red\`
    
  266.         const StyledFactory1 = styled('div')\`color: hotpink\`
    
  267.         const StyledFactory2 = styled('div')({ color: 'hotpink' })
    
  268.         const StyledFactory3 = styled(A)({ color: 'hotpink' })
    
  269.         const FunnyFactory = funny.factory\`\`;
    
  270. 
    
  271.         let Alias1 = A;
    
  272.         let Alias2 = A.Foo;
    
  273.         const Dict = {};
    
  274. 
    
  275.         function Foo() {
    
  276.           return (
    
  277.             <div><A /><B /><StyledFactory1 /><StyledFactory2 /><StyledFactory3 /><Alias1 /><Alias2 /><Header /><Dict.X /></div>
    
  278.           );
    
  279.         }
    
  280. 
    
  281.         const B = hoc(A);
    
  282.         // This is currently registered as a false positive:
    
  283.         const NotAComponent = wow(A);
    
  284.         // We could avoid it but it also doesn't hurt.
    
  285.     `),
    
  286.     ).toMatchSnapshot();
    
  287.   });
    
  288. 
    
  289.   it('registers identifiers used in React.createElement at definition site', () => {
    
  290.     // When in doubt, register variables that were used in JSX.
    
  291.     // Foo, Header, and B get registered.
    
  292.     // A doesn't get registered because it's not declared locally.
    
  293.     // Alias doesn't get registered because its definition is just an identifier.
    
  294.     expect(
    
  295.       transform(`
    
  296.         import A from './A';
    
  297.         import Store from './Store';
    
  298. 
    
  299.         Store.subscribe();
    
  300. 
    
  301.         const Header = styled.div\`color: red\`
    
  302.         const StyledFactory1 = styled('div')\`color: hotpink\`
    
  303.         const StyledFactory2 = styled('div')({ color: 'hotpink' })
    
  304.         const StyledFactory3 = styled(A)({ color: 'hotpink' })
    
  305.         const FunnyFactory = funny.factory\`\`;
    
  306. 
    
  307.         let Alias1 = A;
    
  308.         let Alias2 = A.Foo;
    
  309.         const Dict = {};
    
  310. 
    
  311.         function Foo() {
    
  312.           return [
    
  313.             React.createElement(A),
    
  314.             React.createElement(B),
    
  315.             React.createElement(StyledFactory1),
    
  316.             React.createElement(StyledFactory2),
    
  317.             React.createElement(StyledFactory3),
    
  318.             React.createElement(Alias1),
    
  319.             React.createElement(Alias2),
    
  320.             jsx(Header),
    
  321.             React.createElement(Dict.X),
    
  322.           ];
    
  323.         }
    
  324. 
    
  325.         React.createContext(Store);
    
  326. 
    
  327.         const B = hoc(A);
    
  328.         // This is currently registered as a false positive:
    
  329.         const NotAComponent = wow(A);
    
  330.         // We could avoid it but it also doesn't hurt.
    
  331.     `),
    
  332.     ).toMatchSnapshot();
    
  333.   });
    
  334. 
    
  335.   it('registers capitalized identifiers in HOC calls', () => {
    
  336.     expect(
    
  337.       transform(`
    
  338.         function Foo() {
    
  339.           return <h1>Hi</h1>;
    
  340.         }
    
  341. 
    
  342.         export default hoc(Foo);
    
  343.         export const A = hoc(Foo);
    
  344.         const B = hoc(Foo);
    
  345.     `),
    
  346.     ).toMatchSnapshot();
    
  347.   });
    
  348. 
    
  349.   it('generates signatures for function declarations calling hooks', () => {
    
  350.     expect(
    
  351.       transform(`
    
  352.         export default function App() {
    
  353.           const [foo, setFoo] = useState(0);
    
  354.           React.useEffect(() => {});
    
  355.           return <h1>{foo}</h1>;
    
  356.         }
    
  357.     `),
    
  358.     ).toMatchSnapshot();
    
  359.   });
    
  360. 
    
  361.   it('generates signatures for function expressions calling hooks', () => {
    
  362.     // Unlike __register__, we want to sign all functions -- not just top level.
    
  363.     // This lets us support editing HOCs better.
    
  364.     // For function declarations, __signature__ is called on next line.
    
  365.     // For function expressions, it wraps the expression.
    
  366.     // In order for this to work, __signature__ returns its first argument.
    
  367.     expect(
    
  368.       transform(`
    
  369.         export const A = React.memo(React.forwardRef((props, ref) => {
    
  370.           const [foo, setFoo] = useState(0);
    
  371.           React.useEffect(() => {});
    
  372.           return <h1 ref={ref}>{foo}</h1>;
    
  373.         }));
    
  374. 
    
  375.         export const B = React.memo(React.forwardRef(function(props, ref) {
    
  376.           const [foo, setFoo] = useState(0);
    
  377.           React.useEffect(() => {});
    
  378.           return <h1 ref={ref}>{foo}</h1>;
    
  379.         }));
    
  380. 
    
  381.         function hoc() {
    
  382.           return function Inner() {
    
  383.             const [foo, setFoo] = useState(0);
    
  384.             React.useEffect(() => {});
    
  385.             return <h1 ref={ref}>{foo}</h1>;
    
  386.           };
    
  387.         }
    
  388. 
    
  389.         export let C = hoc();
    
  390.     `),
    
  391.     ).toMatchSnapshot();
    
  392.   });
    
  393. 
    
  394.   it('includes custom hooks into the signatures', () => {
    
  395.     expect(
    
  396.       transform(`
    
  397.         function useFancyState() {
    
  398.           const [foo, setFoo] = React.useState(0);
    
  399.           useFancyEffect();
    
  400.           return foo;
    
  401.         }
    
  402. 
    
  403.         const useFancyEffect = () => {
    
  404.           React.useEffect(() => {});
    
  405.         };
    
  406. 
    
  407.         export default function App() {
    
  408.           const bar = useFancyState();
    
  409.           return <h1>{bar}</h1>;
    
  410.         }
    
  411.     `),
    
  412.     ).toMatchSnapshot();
    
  413.   });
    
  414. 
    
  415.   it('includes custom hooks into the signatures when commonjs target is used', () => {
    
  416.     // this test is passing with Babel 6
    
  417.     // but would fail for Babel 7 _without_ custom hook node being cloned for signature
    
  418.     expect(
    
  419.       transform(
    
  420.         `
    
  421.         import {useFancyState} from './hooks';
    
  422. 
    
  423.         export default function App() {
    
  424.           const bar = useFancyState();
    
  425.           return <h1>{bar}</h1>;
    
  426.         }
    
  427.     `,
    
  428.         {
    
  429.           plugins: ['@babel/transform-modules-commonjs'],
    
  430.         },
    
  431.       ),
    
  432.     ).toMatchSnapshot();
    
  433.   });
    
  434. 
    
  435.   it('generates valid signature for exotic ways to call Hooks', () => {
    
  436.     expect(
    
  437.       transform(`
    
  438.         import FancyHook from 'fancy';
    
  439. 
    
  440.         export default function App() {
    
  441.           function useFancyState() {
    
  442.             const [foo, setFoo] = React.useState(0);
    
  443.             useFancyEffect();
    
  444.             return foo;
    
  445.           }
    
  446.           const bar = useFancyState();
    
  447.           const baz = FancyHook.useThing();
    
  448.           React.useState();
    
  449.           useThePlatform();
    
  450.           return <h1>{bar}{baz}</h1>;
    
  451.         }
    
  452.     `),
    
  453.     ).toMatchSnapshot();
    
  454.   });
    
  455. 
    
  456.   it('does not consider require-like methods to be HOCs', () => {
    
  457.     // None of these were declared in this file.
    
  458.     // It's bad to register them because that would trigger
    
  459.     // modules to execute in an environment with inline requires.
    
  460.     // So we expect the transform to skip all of them even though
    
  461.     // they are used in JSX.
    
  462.     expect(
    
  463.       transform(`
    
  464.         const A = require('A');
    
  465.         const B = foo ? require('X') : require('Y');
    
  466.         const C = requireCond(gk, 'C');
    
  467.         const D = import('D');
    
  468. 
    
  469.         export default function App() {
    
  470.           return (
    
  471.             <div>
    
  472.               <A />
    
  473.               <B />
    
  474.               <C />
    
  475.               <D />
    
  476.             </div>
    
  477.           );
    
  478.         }
    
  479.     `),
    
  480.     ).toMatchSnapshot();
    
  481.   });
    
  482. 
    
  483.   it('can handle implicit arrow returns', () => {
    
  484.     expect(
    
  485.       transform(`
    
  486.         export default () => useContext(X);
    
  487.         export const Foo = () => useContext(X);
    
  488.         module.exports = () => useContext(X);
    
  489.         const Bar = () => useContext(X);
    
  490.         const Baz = memo(() => useContext(X));
    
  491.         const Qux = () => (0, useContext(X));
    
  492.       `),
    
  493.     ).toMatchSnapshot();
    
  494.   });
    
  495. 
    
  496.   it('uses custom identifiers for $RefreshReg$ and $RefreshSig$', () => {
    
  497.     expect(
    
  498.       transform(
    
  499.         `export default function Bar () {
    
  500.         useContext(X)
    
  501.         return <Foo />
    
  502.       };`,
    
  503.         {
    
  504.           freshOptions: {
    
  505.             refreshReg: 'import.meta.refreshReg',
    
  506.             refreshSig: 'import.meta.refreshSig',
    
  507.           },
    
  508.         },
    
  509.       ),
    
  510.     ).toMatchSnapshot();
    
  511.   });
    
  512. 
    
  513.   it("respects Babel's envName option", () => {
    
  514.     const envName = 'random';
    
  515.     expect(() =>
    
  516.       transform(`export default function BabelEnv () { return null };`, {
    
  517.         envName,
    
  518.         skipEnvCheck: false,
    
  519.       }),
    
  520.     ).toThrowError(
    
  521.       'React Refresh Babel transform should only be enabled in development environment. ' +
    
  522.         'Instead, the environment is: "' +
    
  523.         envName +
    
  524.         '". If you want to override this check, pass {skipEnvCheck: true} as plugin options.',
    
  525.     );
    
  526.   });
    
  527. 
    
  528.   it('does not get tripped by IIFEs', () => {
    
  529.     expect(
    
  530.       transform(`
    
  531.         while (item) {
    
  532.           (item => {
    
  533.             useFoo();
    
  534.           })(item);
    
  535.         }
    
  536.       `),
    
  537.     ).toMatchSnapshot();
    
  538.   });
    
  539. 
    
  540.   it('supports typescript namespace syntax', () => {
    
  541.     expect(
    
  542.       transform(
    
  543.         `
    
  544.         namespace Foo {
    
  545.           export namespace Bar {
    
  546.             export const A = () => {};
    
  547. 
    
  548.             function B() {};
    
  549.             export const B1 = B;
    
  550.           }
    
  551. 
    
  552.           export const C = () => {};
    
  553.           export function D() {};
    
  554. 
    
  555.           namespace NotExported {
    
  556.             export const E = () => {};
    
  557.           }
    
  558.         }
    
  559.       `,
    
  560.         {plugins: [['@babel/plugin-syntax-typescript', {isTSX: true}]]},
    
  561.       ),
    
  562.     ).toMatchSnapshot();
    
  563.   });
    
  564. });