[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
This commit is contained in:
Dan Abramov
2019-05-22 23:11:09 +01:00
committed by GitHub
parent 101901dc2d
commit 5c2124fc76
3 changed files with 463 additions and 6 deletions

View File

@@ -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));
});
},
},
},
};
}

View File

@@ -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 <h1 onClick={handleClick}>Hi</h1>;
}
function Bar() {
return <Hello />;
}
`),
).toMatchSnapshot();
});
it('registers top-level exported function declarations', () => {
expect(
transform(`
export function Hello() {
function handleClick() {}
return <h1 onClick={handleClick}>Hi</h1>;
}
export default function Bar() {
return <Hello />;
}
function Baz() {
return <h1>OK</h1>;
}
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 <h1 onClick={handleClick}>Hi</h1>;
};
export let Bar = (props) => <Hello />;
export default () => {
// This one should be ignored.
// You should name your components.
return <Hello />;
};
`),
).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 <h1>Hi</h1>;
}
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 <h1 onClick={handleClick}>Hi</h1>;
};
const Bar = function Baz() {
return <Hello />;
};
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 <h1 onClick={handleClick}>Hi</h1>;
}
const Bar = () => {
return <Hello />;
};
var Baz = () => <div />;
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 <h1 onClick={handleClick}>Hi</h1>;
}
return Comp;
};
function withRouter() {
return function Child() {
const handleClick = () => {};
return <h1 onClick={handleClick}>Hi</h1>;
}
};
`),
).toMatchSnapshot();
});
it('ignores complex definitions', () => {
expect(
transform(`
let A = foo ? () => {
return <h1>Hi</h1>;
} : null
const B = (function Foo() {
return <h1>Hi</h1>;
})();
let C = () => () => {
return <h1>Hi</h1>;
};
let D = bar && (() => {
return <h1>Hi</h1>;
});
`),
).toMatchSnapshot();
});
it('ignores unnamed function declarations', () => {
expect(
transform(`
export default function() {}
`),
).toMatchSnapshot();
});
});

View File

@@ -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 <h1 onClick={handleClick}>Hi</h1>;
}
return Comp;
};
function withRouter() {
return function Child() {
const handleClick = () => {};
return <h1 onClick={handleClick}>Hi</h1>;
};
};"
`;
exports[`ReactFreshBabelPlugin ignores complex definitions 1`] = `
"
let A = foo ? () => {
return <h1>Hi</h1>;
} : null;
const B = function Foo() {
return <h1>Hi</h1>;
}();
let C = () => () => {
return <h1>Hi</h1>;
};
let D = bar && (() => {
return <h1>Hi</h1>;
});"
`;
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 <h1 onClick={handleClick}>Hi</h1>;
}
_c = Hello;
export default function Bar() {
return <Hello />;
}
_c2 = Bar;
function Baz() {
return <h1>OK</h1>;
}
_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 <h1 onClick={handleClick}>Hi</h1>;
};
export let Bar = _c2 = props => <Hello />;
export default (() => {
// This one should be ignored.
// You should name your components.
return <Hello />;
});
var _c, _c2;
__register__(_c, \\"Hello\\");
__register__(_c2, \\"Bar\\");"
`;
exports[`ReactFreshBabelPlugin registers top-level function declarations 1`] = `
"
function Hello() {
function handleClick() {}
return <h1 onClick={handleClick}>Hi</h1>;
}
_c = Hello;
function Bar() {
return <Hello />;
}
_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 <h1 onClick={handleClick}>Hi</h1>;
};
const Bar = _c2 = () => {
return <Hello />;
};
var Baz = _c3 = () => <div />;
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 <h1 onClick={handleClick}>Hi</h1>;
};
const Bar = _c2 = function Baz() {
return <Hello />;
};
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 <h1>Hi</h1>;
}
_c = Hello;
Hello = connect(Hello);
var _c;
__register__(_c, \\"Hello\\");"
`;