main
js 564 lines 15 KB
Raw
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 ).toThrow(
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 });