main
js 212 lines 4.95 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 {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 });