diff --git a/packages/react-fresh/src/ReactFreshBabelPlugin.js b/packages/react-fresh/src/ReactFreshBabelPlugin.js
index 109deb149a..ac7085e9f3 100644
--- a/packages/react-fresh/src/ReactFreshBabelPlugin.js
+++ b/packages/react-fresh/src/ReactFreshBabelPlugin.js
@@ -7,9 +7,128 @@
'use strict';
-// TODO
export default function(babel) {
+ const {types: t, template} = babel;
+
+ const registrationsByProgramPath = new Map();
+ function createRegistration(programPath, persistentID) {
+ const handle = programPath.scope.generateUidIdentifier('c');
+ if (!registrationsByProgramPath.has(programPath)) {
+ registrationsByProgramPath.set(programPath, []);
+ }
+ const registrations = registrationsByProgramPath.get(programPath);
+ registrations.push({
+ handle,
+ persistentID,
+ });
+ return handle;
+ }
+
+ const buildRegistrationCall = template(`
+ __register__(HANDLE, PERSISTENT_ID);
+ `);
+
+ function isComponentishName(name) {
+ return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
+ }
+
+ function isComponentish(node) {
+ switch (node.type) {
+ case 'FunctionDeclaration':
+ return node.id !== null && isComponentishName(node.id.name);
+ case 'VariableDeclarator':
+ return (
+ isComponentishName(node.id.name) &&
+ node.init !== null &&
+ (node.init.type === 'FunctionExpression' ||
+ (node.init.type === 'ArrowFunctionExpression' &&
+ node.init.body.type !== 'ArrowFunctionExpression'))
+ );
+ default:
+ return false;
+ }
+ }
+
return {
- visitor: {},
+ visitor: {
+ FunctionDeclaration(path) {
+ let programPath;
+ let insertAfterPath;
+ switch (path.parent.type) {
+ case 'Program':
+ insertAfterPath = path;
+ programPath = path.parentPath;
+ break;
+ case 'ExportNamedDeclaration':
+ case 'ExportDefaultDeclaration':
+ insertAfterPath = path.parentPath;
+ programPath = insertAfterPath.parentPath;
+ break;
+ default:
+ return;
+ }
+ const maybeComponent = path.node;
+ if (!isComponentish(maybeComponent)) {
+ return;
+ }
+ const functionName = path.node.id.name;
+ const handle = createRegistration(programPath, functionName);
+ insertAfterPath.insertAfter(
+ t.expressionStatement(
+ t.assignmentExpression('=', handle, path.node.id),
+ ),
+ );
+ },
+ VariableDeclaration(path) {
+ let programPath;
+ switch (path.parent.type) {
+ case 'Program':
+ programPath = path.parentPath;
+ break;
+ case 'ExportNamedDeclaration':
+ case 'ExportDefaultDeclaration':
+ programPath = path.parentPath.parentPath;
+ break;
+ default:
+ return;
+ }
+ const declPath = path.get('declarations');
+ if (declPath.length !== 1) {
+ return;
+ }
+ const firstDeclPath = declPath[0];
+ const maybeComponent = firstDeclPath.node;
+ if (!isComponentish(maybeComponent)) {
+ return;
+ }
+ const functionName = maybeComponent.id.name;
+ const initPath = firstDeclPath.get('init');
+ const handle = createRegistration(programPath, functionName);
+ initPath.replaceWith(
+ t.assignmentExpression('=', handle, initPath.node),
+ );
+ },
+ Program: {
+ exit(path) {
+ const registrations = registrationsByProgramPath.get(path);
+ if (registrations === undefined) {
+ return;
+ }
+ registrationsByProgramPath.delete(path);
+ const declarators = [];
+ path.pushContainer('body', t.variableDeclaration('var', declarators));
+ registrations.forEach(({handle, persistentID}) => {
+ path.pushContainer(
+ 'body',
+ buildRegistrationCall({
+ HANDLE: handle,
+ PERSISTENT_ID: t.stringLiteral(persistentID),
+ }),
+ );
+ declarators.push(t.variableDeclarator(handle));
+ });
+ },
+ },
+ },
};
}
diff --git a/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js b/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
index c44cd08af9..b359c2ed19 100644
--- a/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
+++ b/packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
@@ -12,12 +12,178 @@ let freshPlugin = require('react-fresh/babel');
function transform(input, options = {}) {
return babel.transform(input, {
- plugins: [[freshPlugin]],
+ babelrc: false,
+ plugins: ['syntax-jsx', freshPlugin],
}).code;
}
describe('ReactFreshBabelPlugin', () => {
- it('hello world', () => {
- expect(transform(`hello()`)).toMatchSnapshot();
+ it('registers top-level function declarations', () => {
+ // Hello and Bar should be registered, handleClick shouldn't.
+ expect(
+ transform(`
+ function Hello() {
+ function handleClick() {}
+ return
Hi
;
+ }
+
+ function Bar() {
+ return ;
+ }
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers top-level exported function declarations', () => {
+ expect(
+ transform(`
+ export function Hello() {
+ function handleClick() {}
+ return Hi
;
+ }
+
+ export default function Bar() {
+ return ;
+ }
+
+ function Baz() {
+ return OK
;
+ }
+
+ const NotAComp = 'hi';
+ export { Baz, NotAComp };
+
+ export function sum() {}
+ export const Bad = 42;
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers top-level exported named arrow functions', () => {
+ expect(
+ transform(`
+ export const Hello = () => {
+ function handleClick() {}
+ return Hi
;
+ };
+
+ export let Bar = (props) => ;
+
+ export default () => {
+ // This one should be ignored.
+ // You should name your components.
+ return ;
+ };
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('uses original function declaration if it get reassigned', () => {
+ // This should register the original version.
+ // TODO: in the future, we may *also* register the wrapped one.
+ expect(
+ transform(`
+ function Hello() {
+ return Hi
;
+ }
+ Hello = connect(Hello);
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('only registers pascal case functions', () => {
+ // Should not get registered.
+ expect(
+ transform(`
+ function hello() {
+ return 2 * 2;
+ }
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers top-level variable declarations with function expressions', () => {
+ // Hello and Bar should be registered; handleClick, sum, Baz, and Qux shouldn't.
+ expect(
+ transform(`
+ let Hello = function() {
+ function handleClick() {}
+ return Hi
;
+ };
+ const Bar = function Baz() {
+ return ;
+ };
+ function sum() {}
+ let Baz = 10;
+ var Qux;
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers top-level variable declarations with arrow functions', () => {
+ // Hello, Bar, and Baz should be registered; handleClick and sum shouldn't.
+ expect(
+ transform(`
+ let Hello = () => {
+ const handleClick = () => {};
+ return Hi
;
+ }
+ const Bar = () => {
+ return ;
+ };
+ var Baz = () => ;
+ var sum = () => {};
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('ignores HOC definitions', () => {
+ // TODO: we might want to handle HOCs at usage site, however.
+ // TODO: it would be nice if we could always avoid registering
+ // a function that is known to return a function or other non-node.
+ expect(
+ transform(`
+ let connect = () => {
+ function Comp() {
+ const handleClick = () => {};
+ return Hi
;
+ }
+ return Comp;
+ };
+ function withRouter() {
+ return function Child() {
+ const handleClick = () => {};
+ return Hi
;
+ }
+ };
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('ignores complex definitions', () => {
+ expect(
+ transform(`
+ let A = foo ? () => {
+ return Hi
;
+ } : null
+ const B = (function Foo() {
+ return Hi
;
+ })();
+ let C = () => () => {
+ return Hi
;
+ };
+ let D = bar && (() => {
+ return Hi
;
+ });
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('ignores unnamed function declarations', () => {
+ expect(
+ transform(`
+ export default function() {}
+ `),
+ ).toMatchSnapshot();
});
});
diff --git a/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap b/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
index 665cb8949a..273b8c8bd0 100644
--- a/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
+++ b/packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
@@ -1,3 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ReactFreshBabelPlugin hello world 1`] = `"hello();"`;
+exports[`ReactFreshBabelPlugin ignores HOC definitions 1`] = `
+"
+let connect = () => {
+ function Comp() {
+ const handleClick = () => {};
+ return Hi
;
+ }
+ return Comp;
+};
+function withRouter() {
+ return function Child() {
+ const handleClick = () => {};
+ return Hi
;
+ };
+};"
+`;
+
+exports[`ReactFreshBabelPlugin ignores complex definitions 1`] = `
+"
+let A = foo ? () => {
+ return Hi
;
+} : null;
+const B = function Foo() {
+ return Hi
;
+}();
+let C = () => () => {
+ return Hi
;
+};
+let D = bar && (() => {
+ return Hi
;
+});"
+`;
+
+exports[`ReactFreshBabelPlugin ignores unnamed function declarations 1`] = `
+"
+export default function () {}"
+`;
+
+exports[`ReactFreshBabelPlugin only registers pascal case functions 1`] = `
+"
+function hello() {
+ return 2 * 2;
+}"
+`;
+
+exports[`ReactFreshBabelPlugin registers top-level exported function declarations 1`] = `
+"
+export function Hello() {
+ function handleClick() {}
+ return Hi
;
+}
+
+_c = Hello;
+export default function Bar() {
+ return ;
+}
+
+_c2 = Bar;
+function Baz() {
+ return OK
;
+}
+
+_c3 = Baz;
+const NotAComp = 'hi';
+export { Baz, NotAComp };
+
+export function sum() {}
+export const Bad = 42;
+
+var _c, _c2, _c3;
+
+__register__(_c, 'Hello');
+
+__register__(_c2, 'Bar');
+
+__register__(_c3, 'Baz');"
+`;
+
+exports[`ReactFreshBabelPlugin registers top-level exported named arrow functions 1`] = `
+"
+export const Hello = _c = () => {
+ function handleClick() {}
+ return Hi
;
+};
+
+export let Bar = _c2 = props => ;
+
+export default (() => {
+ // This one should be ignored.
+ // You should name your components.
+ return ;
+});
+
+var _c, _c2;
+
+__register__(_c, \\"Hello\\");
+
+__register__(_c2, \\"Bar\\");"
+`;
+
+exports[`ReactFreshBabelPlugin registers top-level function declarations 1`] = `
+"
+function Hello() {
+ function handleClick() {}
+ return Hi
;
+}
+
+_c = Hello;
+function Bar() {
+ return ;
+}
+_c2 = Bar;
+
+var _c, _c2;
+
+__register__(_c, \\"Hello\\");
+
+__register__(_c2, \\"Bar\\");"
+`;
+
+exports[`ReactFreshBabelPlugin registers top-level variable declarations with arrow functions 1`] = `
+"
+let Hello = _c = () => {
+ const handleClick = () => {};
+ return Hi
;
+};
+const Bar = _c2 = () => {
+ return ;
+};
+var Baz = _c3 = () => ;
+var sum = () => {};
+
+var _c, _c2, _c3;
+
+__register__(_c, \\"Hello\\");
+
+__register__(_c2, \\"Bar\\");
+
+__register__(_c3, \\"Baz\\");"
+`;
+
+exports[`ReactFreshBabelPlugin registers top-level variable declarations with function expressions 1`] = `
+"
+let Hello = _c = function () {
+ function handleClick() {}
+ return Hi
;
+};
+const Bar = _c2 = function Baz() {
+ return ;
+};
+function sum() {}
+let Baz = 10;
+var Qux;
+
+var _c, _c2;
+
+__register__(_c, \\"Hello\\");
+
+__register__(_c2, \\"Bar\\");"
+`;
+
+exports[`ReactFreshBabelPlugin uses original function declaration if it get reassigned 1`] = `
+"
+function Hello() {
+ return Hi
;
+}
+_c = Hello;
+Hello = connect(Hello);
+
+var _c;
+
+__register__(_c, \\"Hello\\");"
+`;