main
js 264 lines 7.67 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 * @flow
8 */
9
10 import {parse} from '@babel/parser';
11 import {generateHookMap} from '../generateHookMap';
12 import {getHookNameForLocation} from '../getHookNameForLocation';
13
14 function expectHookMapToEqual(actual, expected) {
15 expect(actual.names).toEqual(expected.names);
16
17 const formattedMappings = [];
18 actual.mappings.forEach(lines => {
19 lines.forEach(segment => {
20 const name = actual.names[segment[2]];
21 if (name == null) {
22 throw new Error(`Expected to find name at position ${segment[2]}`);
23 }
24 formattedMappings.push(`${name} from ${segment[0]}:${segment[1]}`);
25 });
26 });
27 expect(formattedMappings).toEqual(expected.mappings);
28 }
29
30 describe('generateHookMap', () => {
31 it('should parse names for built-in hooks', () => {
32 const code = `
33 import {useState, useContext, useMemo, useReducer} from 'react';
34
35 export function Component() {
36 const a = useMemo(() => A);
37 const [b, setB] = useState(0);
38
39 // prettier-ignore
40 const c = useContext(A), d = useContext(B); // eslint-disable-line one-var
41
42 const [e, dispatch] = useReducer(reducer, initialState);
43 const f = useRef(null)
44
45 return a + b + c + d + e + f.current;
46 }`;
47
48 const parsed = parse(code, {
49 sourceType: 'module',
50 plugins: ['jsx', 'flow'],
51 });
52 const hookMap = generateHookMap(parsed);
53 expectHookMapToEqual(hookMap, {
54 names: ['<no-hook>', 'a', 'b', 'c', 'd', 'e', 'f'],
55 mappings: [
56 '<no-hook> from 1:0',
57 'a from 5:12',
58 '<no-hook> from 5:28',
59 'b from 6:20',
60 '<no-hook> from 6:31',
61 'c from 9:12',
62 '<no-hook> from 9:25',
63 'd from 9:31',
64 '<no-hook> from 9:44',
65 'e from 11:24',
66 '<no-hook> from 11:57',
67 'f from 12:12',
68 '<no-hook> from 12:24',
69 ],
70 });
71
72 expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
73 expect(getHookNameForLocation({line: 2, column: 25}, hookMap)).toEqual(
74 null,
75 );
76 expect(getHookNameForLocation({line: 5, column: 12}, hookMap)).toEqual('a');
77 expect(getHookNameForLocation({line: 5, column: 13}, hookMap)).toEqual('a');
78 expect(getHookNameForLocation({line: 5, column: 28}, hookMap)).toEqual(
79 null,
80 );
81 expect(getHookNameForLocation({line: 5, column: 29}, hookMap)).toEqual(
82 null,
83 );
84 expect(getHookNameForLocation({line: 6, column: 20}, hookMap)).toEqual('b');
85 expect(getHookNameForLocation({line: 6, column: 30}, hookMap)).toEqual('b');
86 expect(getHookNameForLocation({line: 6, column: 31}, hookMap)).toEqual(
87 null,
88 );
89 expect(getHookNameForLocation({line: 7, column: 31}, hookMap)).toEqual(
90 null,
91 );
92 expect(getHookNameForLocation({line: 8, column: 20}, hookMap)).toEqual(
93 null,
94 );
95 expect(getHookNameForLocation({line: 9, column: 12}, hookMap)).toEqual('c');
96 expect(getHookNameForLocation({line: 9, column: 13}, hookMap)).toEqual('c');
97 expect(getHookNameForLocation({line: 9, column: 25}, hookMap)).toEqual(
98 null,
99 );
100 expect(getHookNameForLocation({line: 9, column: 26}, hookMap)).toEqual(
101 null,
102 );
103 expect(getHookNameForLocation({line: 9, column: 31}, hookMap)).toEqual('d');
104 expect(getHookNameForLocation({line: 9, column: 32}, hookMap)).toEqual('d');
105 expect(getHookNameForLocation({line: 9, column: 44}, hookMap)).toEqual(
106 null,
107 );
108 expect(getHookNameForLocation({line: 9, column: 45}, hookMap)).toEqual(
109 null,
110 );
111 expect(getHookNameForLocation({line: 11, column: 24}, hookMap)).toEqual(
112 'e',
113 );
114 expect(getHookNameForLocation({line: 11, column: 56}, hookMap)).toEqual(
115 'e',
116 );
117 expect(getHookNameForLocation({line: 11, column: 57}, hookMap)).toEqual(
118 null,
119 );
120 expect(getHookNameForLocation({line: 11, column: 58}, hookMap)).toEqual(
121 null,
122 );
123 expect(getHookNameForLocation({line: 12, column: 12}, hookMap)).toEqual(
124 'f',
125 );
126 expect(getHookNameForLocation({line: 12, column: 23}, hookMap)).toEqual(
127 'f',
128 );
129 expect(getHookNameForLocation({line: 12, column: 24}, hookMap)).toEqual(
130 null,
131 );
132 expect(getHookNameForLocation({line: 100, column: 50}, hookMap)).toEqual(
133 null,
134 );
135 });
136
137 it('should parse names for custom hooks', () => {
138 const code = `
139 import useTheme from 'useTheme';
140 import useValue from 'useValue';
141
142 export function Component() {
143 const theme = useTheme();
144 const [val, setVal] = useValue();
145
146 return theme;
147 }`;
148
149 const parsed = parse(code, {
150 sourceType: 'module',
151 plugins: ['jsx', 'flow'],
152 });
153 const hookMap = generateHookMap(parsed);
154 expectHookMapToEqual(hookMap, {
155 names: ['<no-hook>', 'theme', 'val'],
156 mappings: [
157 '<no-hook> from 1:0',
158 'theme from 6:16',
159 '<no-hook> from 6:26',
160 'val from 7:24',
161 '<no-hook> from 7:34',
162 ],
163 });
164
165 expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
166 expect(getHookNameForLocation({line: 6, column: 16}, hookMap)).toEqual(
167 'theme',
168 );
169 expect(getHookNameForLocation({line: 6, column: 26}, hookMap)).toEqual(
170 null,
171 );
172 expect(getHookNameForLocation({line: 7, column: 24}, hookMap)).toEqual(
173 'val',
174 );
175 expect(getHookNameForLocation({line: 7, column: 34}, hookMap)).toEqual(
176 null,
177 );
178 });
179
180 it('should parse names for nested hook calls', () => {
181 const code = `
182 import {useMemo, useState} from 'react';
183
184 export function Component() {
185 const InnerComponent = useMemo(() => () => {
186 const [state, setState] = useState(0);
187
188 return state;
189 });
190
191 return null;
192 }`;
193
194 const parsed = parse(code, {
195 sourceType: 'module',
196 plugins: ['jsx', 'flow'],
197 });
198 const hookMap = generateHookMap(parsed);
199 expectHookMapToEqual(hookMap, {
200 names: ['<no-hook>', 'InnerComponent', 'state'],
201 mappings: [
202 '<no-hook> from 1:0',
203 'InnerComponent from 5:25',
204 'state from 6:30',
205 'InnerComponent from 6:41',
206 '<no-hook> from 9:4',
207 ],
208 });
209
210 expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
211 expect(getHookNameForLocation({line: 5, column: 25}, hookMap)).toEqual(
212 'InnerComponent',
213 );
214 expect(getHookNameForLocation({line: 6, column: 30}, hookMap)).toEqual(
215 'state',
216 );
217 expect(getHookNameForLocation({line: 6, column: 40}, hookMap)).toEqual(
218 'state',
219 );
220 expect(getHookNameForLocation({line: 6, column: 41}, hookMap)).toEqual(
221 'InnerComponent',
222 );
223 expect(getHookNameForLocation({line: 9, column: 4}, hookMap)).toEqual(null);
224 });
225
226 it('should skip names for non-nameable hooks', () => {
227 const code = `
228 import useTheme from 'useTheme';
229 import useValue from 'useValue';
230
231 export function Component() {
232 const [val, setVal] = useState(0);
233
234 useEffect(() => {
235 // ...
236 });
237
238 useLayoutEffect(() => {
239 // ...
240 });
241
242 return val;
243 }`;
244
245 const parsed = parse(code, {
246 sourceType: 'module',
247 plugins: ['jsx', 'flow'],
248 });
249 const hookMap = generateHookMap(parsed);
250 expectHookMapToEqual(hookMap, {
251 names: ['<no-hook>', 'val'],
252 mappings: ['<no-hook> from 1:0', 'val from 6:24', '<no-hook> from 6:35'],
253 });
254
255 expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
256 expect(getHookNameForLocation({line: 6, column: 24}, hookMap)).toEqual(
257 'val',
258 );
259 expect(getHookNameForLocation({line: 6, column: 35}, hookMap)).toEqual(
260 null,
261 );
262 expect(getHookNameForLocation({line: 8, column: 2}, hookMap)).toEqual(null);
263 });
264 });