@samitouri / QOS-React-2 / commits / 5c2124fc76

[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 +`;