Skip to content

Commit 5c2124f

Browse files
authored
[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
1 parent 101901d commit 5c2124f

File tree

3 files changed

+463
-6
lines changed

3 files changed

+463
-6
lines changed

packages/react-fresh/src/ReactFreshBabelPlugin.js

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,128 @@
77

88
'use strict';
99

10-
// TODO
1110
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+
1252
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+
},
14133
};
15134
}

packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,178 @@ let freshPlugin = require('react-fresh/babel');
1212

1313
function transform(input, options = {}) {
1414
return babel.transform(input, {
15-
plugins: [[freshPlugin]],
15+
babelrc: false,
16+
plugins: ['syntax-jsx', freshPlugin],
1617
}).code;
1718
}
1819

1920
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();
22188
});
23189
});

0 commit comments

Comments
 (0)