[Fresh] Initial Babel plugin implementation (#15711)
* Add initial Babel plugin implementation * Register exported functions * Fix missing declarations Always declare them at the bottom and rely on hoisting. * Remove unused code * Don't pass filename to tests I've decided for now that the plugin doesn't need filename, and it will be handled by module runtime integration instead. * Fix bugs * Coalesce variable declarations
Dan Abramov committed
May 22, 2019 at 23:11 UTC
5c2124fc76e59fb4fa805d9543afdd7b6866cb5e
3 files changed
+463
-6
packages/react-fresh/src/ReactFreshBabelPlugin.js
+121
-2
@@ -7,9 +7,128 @@
7
8
'use strict';
9
10
-// TODO
10
export default function(babel) {
11
+ const {types: t, template} = babel;
12
+
13
+ const registrationsByProgramPath = new Map();
14
+ function createRegistration(programPath, persistentID) {
15
+ const handle = programPath.scope.generateUidIdentifier('c');
16
+ if (!registrationsByProgramPath.has(programPath)) {
17
+ registrationsByProgramPath.set(programPath, []);
18
+ }
19
+ const registrations = registrationsByProgramPath.get(programPath);
20
+ registrations.push({
21
+ handle,
22
+ persistentID,
23
+ });
24
+ return handle;
25
+ }
26
+
27
+ const buildRegistrationCall = template(`
28
+ __register__(HANDLE, PERSISTENT_ID);
29
+ `);
30
+
31
+ function isComponentishName(name) {
32
+ return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
33
+ }
34
+
35
+ function isComponentish(node) {
36
+ switch (node.type) {
37
+ case 'FunctionDeclaration':
38
+ return node.id !== null && isComponentishName(node.id.name);
39
+ case 'VariableDeclarator':
40
+ return (
41
+ isComponentishName(node.id.name) &&
42
+ node.init !== null &&
43
+ (node.init.type === 'FunctionExpression' ||
44
+ (node.init.type === 'ArrowFunctionExpression' &&
45
+ node.init.body.type !== 'ArrowFunctionExpression'))
46
+ );
47
+ default:
48
+ return false;
49
+ }
50
+ }
51
+
52
return {
13
- visitor: {},
53
+ visitor: {
54
+ FunctionDeclaration(path) {
55
+ let programPath;
56
+ let insertAfterPath;
57
+ switch (path.parent.type) {
58
+ case 'Program':
59
+ insertAfterPath = path;
60
+ programPath = path.parentPath;
61
+ break;
62
+ case 'ExportNamedDeclaration':
63
+ case 'ExportDefaultDeclaration':
64
+ insertAfterPath = path.parentPath;
65
+ programPath = insertAfterPath.parentPath;
66
+ break;
67
+ default:
68
+ return;
69
+ }
70
+ const maybeComponent = path.node;
71
+ if (!isComponentish(maybeComponent)) {
72
+ return;
73
+ }
74
+ const functionName = path.node.id.name;
75
+ const handle = createRegistration(programPath, functionName);
76
+ insertAfterPath.insertAfter(
77
+ t.expressionStatement(
78
+ t.assignmentExpression('=', handle, path.node.id),
79
+ ),
80
+ );
81
+ },
82
+ VariableDeclaration(path) {
83
+ let programPath;
84
+ switch (path.parent.type) {
85
+ case 'Program':
86
+ programPath = path.parentPath;
87
+ break;
88
+ case 'ExportNamedDeclaration':
89
+ case 'ExportDefaultDeclaration':
90
+ programPath = path.parentPath.parentPath;
91
+ break;
92
+ default:
93
+ return;
94
+ }
95
+ const declPath = path.get('declarations');
96
+ if (declPath.length !== 1) {
97
+ return;
98
+ }
99
+ const firstDeclPath = declPath[0];
100
+ const maybeComponent = firstDeclPath.node;
101
+ if (!isComponentish(maybeComponent)) {
102
+ return;
103
+ }
104
+ const functionName = maybeComponent.id.name;
105
+ const initPath = firstDeclPath.get('init');
106
+ const handle = createRegistration(programPath, functionName);
107
+ initPath.replaceWith(
108
+ t.assignmentExpression('=', handle, initPath.node),
109
+ );
110
+ },
111
+ Program: {
112
+ exit(path) {
113
+ const registrations = registrationsByProgramPath.get(path);
114
+ if (registrations === undefined) {
115
+ return;
116
+ }
117
+ registrationsByProgramPath.delete(path);
118
+ const declarators = [];
119
+ path.pushContainer('body', t.variableDeclaration('var', declarators));
120
+ registrations.forEach(({handle, persistentID}) => {
121
+ path.pushContainer(
122
+ 'body',
123
+ buildRegistrationCall({
124
+ HANDLE: handle,
125
+ PERSISTENT_ID: t.stringLiteral(persistentID),
126
+ }),
127
+ );
128
+ declarators.push(t.variableDeclarator(handle));
129
+ });
130
+ },
131
+ },
132
+ },
133
};
134
}
packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
+169
-3
@@ -12,12 +12,178 @@ let freshPlugin = require('react-fresh/babel');
12
13
function transform(input, options = {}) {
14
return babel.transform(input, {
15
- plugins: [[freshPlugin]],
15
+ babelrc: false,
16
+ plugins: ['syntax-jsx', freshPlugin],
17
}).code;
18
}
19
20
describe('ReactFreshBabelPlugin', () => {
20
- it('hello world', () => {
21
- expect(transform(`hello()`)).toMatchSnapshot();
21
+ it('registers top-level function declarations', () => {
22
+ // Hello and Bar should be registered, handleClick shouldn't.
23
+ expect(
24
+ transform(`
25
+ function Hello() {
26
+ function handleClick() {}
27
+ return <h1 onClick={handleClick}>Hi</h1>;
28
+ }
29
+
30
+ function Bar() {
31
+ return <Hello />;
32
+ }
33
+ `),
34
+ ).toMatchSnapshot();
35
+ });
36
+
37
+ it('registers top-level exported function declarations', () => {
38
+ expect(
39
+ transform(`
40
+ export function Hello() {
41
+ function handleClick() {}
42
+ return <h1 onClick={handleClick}>Hi</h1>;
43
+ }
44
+
45
+ export default function Bar() {
46
+ return <Hello />;
47
+ }
48
+
49
+ function Baz() {
50
+ return <h1>OK</h1>;
51
+ }
52
+
53
+ const NotAComp = 'hi';
54
+ export { Baz, NotAComp };
55
+
56
+ export function sum() {}
57
+ export const Bad = 42;
58
+ `),
59
+ ).toMatchSnapshot();
60
+ });
61
+
62
+ it('registers top-level exported named arrow functions', () => {
63
+ expect(
64
+ transform(`
65
+ export const Hello = () => {
66
+ function handleClick() {}
67
+ return <h1 onClick={handleClick}>Hi</h1>;
68
+ };
69
+
70
+ export let Bar = (props) => <Hello />;
71
+
72
+ export default () => {
73
+ // This one should be ignored.
74
+ // You should name your components.
75
+ return <Hello />;
76
+ };
77
+ `),
78
+ ).toMatchSnapshot();
79
+ });
80
+
81
+ it('uses original function declaration if it get reassigned', () => {
82
+ // This should register the original version.
83
+ // TODO: in the future, we may *also* register the wrapped one.
84
+ expect(
85
+ transform(`
86
+ function Hello() {
87
+ return <h1>Hi</h1>;
88
+ }
89
+ Hello = connect(Hello);
90
+ `),
91
+ ).toMatchSnapshot();
92
+ });
93
+
94
+ it('only registers pascal case functions', () => {
95
+ // Should not get registered.
96
+ expect(
97
+ transform(`
98
+ function hello() {
99
+ return 2 * 2;
100
+ }
101
+ `),
102
+ ).toMatchSnapshot();
103
+ });
104
+
105
+ it('registers top-level variable declarations with function expressions', () => {
106
+ // Hello and Bar should be registered; handleClick, sum, Baz, and Qux shouldn't.
107
+ expect(
108
+ transform(`
109
+ let Hello = function() {
110
+ function handleClick() {}
111
+ return <h1 onClick={handleClick}>Hi</h1>;
112
+ };
113
+ const Bar = function Baz() {
114
+ return <Hello />;
115
+ };
116
+ function sum() {}
117
+ let Baz = 10;
118
+ var Qux;
119
+ `),
120
+ ).toMatchSnapshot();
121
+ });
122
+
123
+ it('registers top-level variable declarations with arrow functions', () => {
124
+ // Hello, Bar, and Baz should be registered; handleClick and sum shouldn't.
125
+ expect(
126
+ transform(`
127
+ let Hello = () => {
128
+ const handleClick = () => {};
129
+ return <h1 onClick={handleClick}>Hi</h1>;
130
+ }
131
+ const Bar = () => {
132
+ return <Hello />;
133
+ };
134
+ var Baz = () => <div />;
135
+ var sum = () => {};
136
+ `),
137
+ ).toMatchSnapshot();
138
+ });
139
+
140
+ it('ignores HOC definitions', () => {
141
+ // TODO: we might want to handle HOCs at usage site, however.
142
+ // TODO: it would be nice if we could always avoid registering
143
+ // a function that is known to return a function or other non-node.
144
+ expect(
145
+ transform(`
146
+ let connect = () => {
147
+ function Comp() {
148
+ const handleClick = () => {};
149
+ return <h1 onClick={handleClick}>Hi</h1>;
150
+ }
151
+ return Comp;
152
+ };
153
+ function withRouter() {
154
+ return function Child() {
155
+ const handleClick = () => {};
156
+ return <h1 onClick={handleClick}>Hi</h1>;
157
+ }
158
+ };
159
+ `),
160
+ ).toMatchSnapshot();
161
+ });
162
+
163
+ it('ignores complex definitions', () => {
164
+ expect(
165
+ transform(`
166
+ let A = foo ? () => {
167
+ return <h1>Hi</h1>;
168
+ } : null
169
+ const B = (function Foo() {
170
+ return <h1>Hi</h1>;
171
+ })();
172
+ let C = () => () => {
173
+ return <h1>Hi</h1>;
174
+ };
175
+ let D = bar && (() => {
176
+ return <h1>Hi</h1>;
177
+ });
178
+ `),
179
+ ).toMatchSnapshot();
180
+ });
181
+
182
+ it('ignores unnamed function declarations', () => {
183
+ expect(
184
+ transform(`
185
+ export default function() {}
186
+ `),
187
+ ).toMatchSnapshot();
188
});
189
});
packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
+173
-1
@@ -1,3 +1,175 @@
1
// Jest Snapshot v1, https://goo.gl/fbAQLP
2
3
-exports[`ReactFreshBabelPlugin hello world 1`] = `"hello();"`;
3
+exports[`ReactFreshBabelPlugin ignores HOC definitions 1`] = `
4
+"
5
+let connect = () => {
6
+ function Comp() {
7
+ const handleClick = () => {};
8
+ return <h1 onClick={handleClick}>Hi</h1>;
9
+ }
10
+ return Comp;
11
+};
12
+function withRouter() {
13
+ return function Child() {
14
+ const handleClick = () => {};
15
+ return <h1 onClick={handleClick}>Hi</h1>;
16
+ };
17
+};"
18
+`;
19
+
20
+exports[`ReactFreshBabelPlugin ignores complex definitions 1`] = `
21
+"
22
+let A = foo ? () => {
23
+ return <h1>Hi</h1>;
24
+} : null;
25
+const B = function Foo() {
26
+ return <h1>Hi</h1>;
27
+}();
28
+let C = () => () => {
29
+ return <h1>Hi</h1>;
30
+};
31
+let D = bar && (() => {
32
+ return <h1>Hi</h1>;
33
+});"
34
+`;
35
+
36
+exports[`ReactFreshBabelPlugin ignores unnamed function declarations 1`] = `
37
+"
38
+export default function () {}"
39
+`;
40
+
41
+exports[`ReactFreshBabelPlugin only registers pascal case functions 1`] = `
42
+"
43
+function hello() {
44
+ return 2 * 2;
45
+}"
46
+`;
47
+
48
+exports[`ReactFreshBabelPlugin registers top-level exported function declarations 1`] = `
49
+"
50
+export function Hello() {
51
+ function handleClick() {}
52
+ return <h1 onClick={handleClick}>Hi</h1>;
53
+}
54
+
55
+_c = Hello;
56
+export default function Bar() {
57
+ return <Hello />;
58
+}
59
+
60
+_c2 = Bar;
61
+function Baz() {
62
+ return <h1>OK</h1>;
63
+}
64
+
65
+_c3 = Baz;
66
+const NotAComp = 'hi';
67
+export { Baz, NotAComp };
68
+
69
+export function sum() {}
70
+export const Bad = 42;
71
+
72
+var _c, _c2, _c3;
73
+
74
+__register__(_c, 'Hello');
75
+
76
+__register__(_c2, 'Bar');
77
+
78
+__register__(_c3, 'Baz');"
79
+`;
80
+
81
+exports[`ReactFreshBabelPlugin registers top-level exported named arrow functions 1`] = `
82
+"
83
+export const Hello = _c = () => {
84
+ function handleClick() {}
85
+ return <h1 onClick={handleClick}>Hi</h1>;
86
+};
87
+
88
+export let Bar = _c2 = props => <Hello />;
89
+
90
+export default (() => {
91
+ // This one should be ignored.
92
+ // You should name your components.
93
+ return <Hello />;
94
+});
95
+
96
+var _c, _c2;
97
+
98
+__register__(_c, \\"Hello\\");
99
+
100
+__register__(_c2, \\"Bar\\");"
101
+`;
102
+
103
+exports[`ReactFreshBabelPlugin registers top-level function declarations 1`] = `
104
+"
105
+function Hello() {
106
+ function handleClick() {}
107
+ return <h1 onClick={handleClick}>Hi</h1>;
108
+}
109
+
110
+_c = Hello;
111
+function Bar() {
112
+ return <Hello />;
113
+}
114
+_c2 = Bar;
115
+
116
+var _c, _c2;
117
+
118
+__register__(_c, \\"Hello\\");
119
+
120
+__register__(_c2, \\"Bar\\");"
121
+`;
122
+
123
+exports[`ReactFreshBabelPlugin registers top-level variable declarations with arrow functions 1`] = `
124
+"
125
+let Hello = _c = () => {
126
+ const handleClick = () => {};
127
+ return <h1 onClick={handleClick}>Hi</h1>;
128
+};
129
+const Bar = _c2 = () => {
130
+ return <Hello />;
131
+};
132
+var Baz = _c3 = () => <div />;
133
+var sum = () => {};
134
+
135
+var _c, _c2, _c3;
136
+
137
+__register__(_c, \\"Hello\\");
138
+
139
+__register__(_c2, \\"Bar\\");
140
+
141
+__register__(_c3, \\"Baz\\");"
142
+`;
143
+
144
+exports[`ReactFreshBabelPlugin registers top-level variable declarations with function expressions 1`] = `
145
+"
146
+let Hello = _c = function () {
147
+ function handleClick() {}
148
+ return <h1 onClick={handleClick}>Hi</h1>;
149
+};
150
+const Bar = _c2 = function Baz() {
151
+ return <Hello />;
152
+};
153
+function sum() {}
154
+let Baz = 10;
155
+var Qux;
156
+
157
+var _c, _c2;
158
+
159
+__register__(_c, \\"Hello\\");
160
+
161
+__register__(_c2, \\"Bar\\");"
162
+`;
163
+
164
+exports[`ReactFreshBabelPlugin uses original function declaration if it get reassigned 1`] = `
165
+"
166
+function Hello() {
167
+ return <h1>Hi</h1>;
168
+}
169
+_c = Hello;
170
+Hello = connect(Hello);
171
+
172
+var _c;
173
+
174
+__register__(_c, \\"Hello\\");"
175
+`;