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.  * @flow
    
  8.  */
    
  9. 
    
  10. import {parse} from '@babel/parser';
    
  11. import {generateEncodedHookMap, generateHookMap} from '../generateHookMap';
    
  12. 
    
  13. function expectHookMapToEqual(actual, expected) {
    
  14.   expect(actual.names).toEqual(expected.names);
    
  15. 
    
  16.   const formattedMappings = [];
    
  17.   actual.mappings.forEach(lines => {
    
  18.     lines.forEach(segment => {
    
  19.       const name = actual.names[segment[2]];
    
  20.       if (name == null) {
    
  21.         throw new Error(`Expected to find name at position ${segment[2]}`);
    
  22.       }
    
  23.       formattedMappings.push(`${name} from ${segment[0]}:${segment[1]}`);
    
  24.     });
    
  25.   });
    
  26.   expect(formattedMappings).toEqual(expected.mappings);
    
  27. }
    
  28. 
    
  29. describe('generateHookMap', () => {
    
  30.   it('should parse names for built-in hooks', () => {
    
  31.     const code = `
    
  32. import {useState, useContext, useMemo, useReducer} from 'react';
    
  33. 
    
  34. export function Component() {
    
  35.   const a = useMemo(() => A);
    
  36.   const [b, setB] = useState(0);
    
  37. 
    
  38.   // prettier-ignore
    
  39.   const c = useContext(A), d = useContext(B); // eslint-disable-line one-var
    
  40. 
    
  41.   const [e, dispatch] = useReducer(reducer, initialState);
    
  42.   const f = useRef(null)
    
  43. 
    
  44.   return a + b + c + d + e + f.current;
    
  45. }`;
    
  46. 
    
  47.     const parsed = parse(code, {
    
  48.       sourceType: 'module',
    
  49.       plugins: ['jsx', 'flow'],
    
  50.     });
    
  51.     const hookMap = generateHookMap(parsed);
    
  52.     expectHookMapToEqual(hookMap, {
    
  53.       names: ['<no-hook>', 'a', 'b', 'c', 'd', 'e', 'f'],
    
  54.       mappings: [
    
  55.         '<no-hook> from 1:0',
    
  56.         'a from 5:12',
    
  57.         '<no-hook> from 5:28',
    
  58.         'b from 6:20',
    
  59.         '<no-hook> from 6:31',
    
  60.         'c from 9:12',
    
  61.         '<no-hook> from 9:25',
    
  62.         'd from 9:31',
    
  63.         '<no-hook> from 9:44',
    
  64.         'e from 11:24',
    
  65.         '<no-hook> from 11:57',
    
  66.         'f from 12:12',
    
  67.         '<no-hook> from 12:24',
    
  68.       ],
    
  69.     });
    
  70. 
    
  71.     const encodedHookMap = generateEncodedHookMap(parsed);
    
  72.     expect(encodedHookMap).toMatchInlineSnapshot(`
    
  73.     {
    
  74.       "mappings": "CAAD;KYCA,AgBDA;MREA,AWFA;SnBGA,AaHA,AMIA,AaJA;WpBKA,AiCLA;Y7CMA,AYNA",
    
  75.       "names": [
    
  76.         "<no-hook>",
    
  77.         "a",
    
  78.         "b",
    
  79.         "c",
    
  80.         "d",
    
  81.         "e",
    
  82.         "f",
    
  83.       ],
    
  84.     }
    
  85.   `);
    
  86.   });
    
  87. 
    
  88.   it('should parse names for custom hooks', () => {
    
  89.     const code = `
    
  90. import useTheme from 'useTheme';
    
  91. import useValue from 'useValue';
    
  92. 
    
  93. export function Component() {
    
  94.   const theme = useTheme();
    
  95.   const [val, setVal] = useValue();
    
  96. 
    
  97.   return theme;
    
  98. }`;
    
  99. 
    
  100.     const parsed = parse(code, {
    
  101.       sourceType: 'module',
    
  102.       plugins: ['jsx', 'flow'],
    
  103.     });
    
  104.     const hookMap = generateHookMap(parsed);
    
  105.     expectHookMapToEqual(hookMap, {
    
  106.       names: ['<no-hook>', 'theme', 'val'],
    
  107.       mappings: [
    
  108.         '<no-hook> from 1:0',
    
  109.         'theme from 6:16',
    
  110.         '<no-hook> from 6:26',
    
  111.         'val from 7:24',
    
  112.         '<no-hook> from 7:34',
    
  113.       ],
    
  114.     });
    
  115. 
    
  116.     const encodedHookMap = generateEncodedHookMap(parsed);
    
  117.     expect(encodedHookMap).toMatchInlineSnapshot(`
    
  118.     {
    
  119.       "mappings": "CAAD;MgBCA,AUDA;OFEA,AUFA",
    
  120.       "names": [
    
  121.         "<no-hook>",
    
  122.         "theme",
    
  123.         "val",
    
  124.       ],
    
  125.     }
    
  126.   `);
    
  127.   });
    
  128. 
    
  129.   it('should parse names for nested hook calls', () => {
    
  130.     const code = `
    
  131. import {useMemo, useState} from 'react';
    
  132. 
    
  133. export function Component() {
    
  134.   const InnerComponent = useMemo(() => () => {
    
  135.     const [state, setState] = useState(0);
    
  136. 
    
  137.     return state;
    
  138.   });
    
  139. 
    
  140.   return null;
    
  141. }`;
    
  142. 
    
  143.     const parsed = parse(code, {
    
  144.       sourceType: 'module',
    
  145.       plugins: ['jsx', 'flow'],
    
  146.     });
    
  147.     const hookMap = generateHookMap(parsed);
    
  148.     expectHookMapToEqual(hookMap, {
    
  149.       names: ['<no-hook>', 'InnerComponent', 'state'],
    
  150.       mappings: [
    
  151.         '<no-hook> from 1:0',
    
  152.         'InnerComponent from 5:25',
    
  153.         'state from 6:30',
    
  154.         'InnerComponent from 6:41',
    
  155.         '<no-hook> from 9:4',
    
  156.       ],
    
  157.     });
    
  158. 
    
  159.     const encodedHookMap = generateEncodedHookMap(parsed);
    
  160.     expect(encodedHookMap).toMatchInlineSnapshot(`
    
  161.     {
    
  162.       "mappings": "CAAD;KyBCA;MKCA,AWDA;SrCDA",
    
  163.       "names": [
    
  164.         "<no-hook>",
    
  165.         "InnerComponent",
    
  166.         "state",
    
  167.       ],
    
  168.     }
    
  169.   `);
    
  170.   });
    
  171. 
    
  172.   it('should skip names for non-nameable hooks', () => {
    
  173.     const code = `
    
  174. import useTheme from 'useTheme';
    
  175. import useValue from 'useValue';
    
  176. 
    
  177. export function Component() {
    
  178.   const [val, setVal] = useState(0);
    
  179. 
    
  180.   useEffect(() => {
    
  181.     // ...
    
  182.   });
    
  183. 
    
  184.   useLayoutEffect(() => {
    
  185.     // ...
    
  186.   });
    
  187. 
    
  188.   return val;
    
  189. }`;
    
  190. 
    
  191.     const parsed = parse(code, {
    
  192.       sourceType: 'module',
    
  193.       plugins: ['jsx', 'flow'],
    
  194.     });
    
  195.     const hookMap = generateHookMap(parsed);
    
  196.     expectHookMapToEqual(hookMap, {
    
  197.       names: ['<no-hook>', 'val'],
    
  198.       mappings: ['<no-hook> from 1:0', 'val from 6:24', '<no-hook> from 6:35'],
    
  199.     });
    
  200. 
    
  201.     const encodedHookMap = generateEncodedHookMap(parsed);
    
  202.     expect(encodedHookMap).toMatchInlineSnapshot(`
    
  203.     {
    
  204.       "mappings": "CAAD;MwBCA,AWDA",
    
  205.       "names": [
    
  206.         "<no-hook>",
    
  207.         "val",
    
  208.       ],
    
  209.     }
    
  210.   `);
    
  211.   });
    
  212. });