Export the hmr plugin directly

Summary: This will allow to configure the HMR plugin directly from `.babelrc` again, instead of having to create a bridge file: https://github.com/rafeca/metro-sample-app/blob/master/metro-babel7-plugin-react-transform.js#L3

Reviewed By: davidaurelio

Differential Revision: D7707314

fbshipit-source-id: 4c5612e1e5d27874807f2dce50d99ec0f6354bbc
This commit is contained in:
Rafael Oleza 2018-04-20 09:17:39 -07:00 committed by Facebook Github Bot
parent a8ce776044
commit a6b61554ec
3 changed files with 364 additions and 373 deletions

View File

@ -18,7 +18,7 @@ const path = require('path');
/*eslint-disable import/no-extraneous-dependencies*/ /*eslint-disable import/no-extraneous-dependencies*/
const {transformSync} = require('@babel/core'); const {transformSync} = require('@babel/core');
const reactPlugin = require('../lib/index.js').default; const reactPlugin = require('../lib/index.js');
describe('finds React components', () => { describe('finds React components', () => {
const fixturesDir = path.join(__dirname, '__fixtures__'); const fixturesDir = path.join(__dirname, '__fixtures__');

View File

@ -28,71 +28,70 @@ const find = require('lodash/find');
const {addDefault} = require('@babel/helper-module-imports'); const {addDefault} = require('@babel/helper-module-imports');
module.exports = { module.exports = function({types: t, template}) {
default: function({types: t, template}) { function matchesPatterns(path, patterns) {
function matchesPatterns(path, patterns) { return !!find(patterns, pattern => {
return !!find(patterns, pattern => {
return (
t.isIdentifier(path.node, {name: pattern}) ||
path.matchesPattern(pattern)
);
});
}
function isReactLikeClass(node) {
return !!find(node.body.body, classMember => {
return (
t.isClassMethod(classMember) &&
t.isIdentifier(classMember.key, {name: 'render'})
);
});
}
function isReactLikeComponentObject(node) {
return ( return (
t.isObjectExpression(node) && t.isIdentifier(path.node, {name: pattern}) ||
!!find(node.properties, objectMember => { path.matchesPattern(pattern)
return (
(t.isObjectProperty(objectMember) ||
t.isObjectMethod(objectMember)) &&
(t.isIdentifier(objectMember.key, {name: 'render'}) ||
t.isStringLiteral(objectMember.key, {value: 'render'}))
);
})
); );
} });
}
// `foo({ displayName: 'NAME' });` => 'NAME' function isReactLikeClass(node) {
function getDisplayName(node) { return !!find(node.body.body, classMember => {
const property = find( return (
node.arguments[0].properties, t.isClassMethod(classMember) &&
_node => _node.key.name === 'displayName', t.isIdentifier(classMember.key, {name: 'render'})
); );
return property && property.value.value; });
} }
function hasParentFunction(path) { function isReactLikeComponentObject(node) {
return !!path.findParent(parentPath => parentPath.isFunction()); return (
} t.isObjectExpression(node) &&
!!find(node.properties, objectMember => {
return (
(t.isObjectProperty(objectMember) ||
t.isObjectMethod(objectMember)) &&
(t.isIdentifier(objectMember.key, {name: 'render'}) ||
t.isStringLiteral(objectMember.key, {value: 'render'}))
);
})
);
}
// wrapperFunction("componentId")(node) // `foo({ displayName: 'NAME' });` => 'NAME'
function wrapComponent(node, componentId, wrapperFunctionId) { function getDisplayName(node) {
return t.callExpression( const property = find(
t.callExpression(wrapperFunctionId, [t.stringLiteral(componentId)]), node.arguments[0].properties,
[node], _node => _node.key.name === 'displayName',
); );
} return property && property.value.value;
}
// `{ name: foo }` => Node { type: "ObjectExpression", properties: [...] } function hasParentFunction(path) {
function toObjectExpression(object) { return !!path.findParent(parentPath => parentPath.isFunction());
const properties = Object.keys(object).map(key => { }
return t.objectProperty(t.identifier(key), object[key]);
});
return t.objectExpression(properties); // wrapperFunction("componentId")(node)
} function wrapComponent(node, componentId, wrapperFunctionId) {
return t.callExpression(
t.callExpression(wrapperFunctionId, [t.stringLiteral(componentId)]),
[node],
);
}
const wrapperFunctionTemplate = template(` // `{ name: foo }` => Node { type: "ObjectExpression", properties: [...] }
function toObjectExpression(object) {
const properties = Object.keys(object).map(key => {
return t.objectProperty(t.identifier(key), object[key]);
});
return t.objectExpression(properties);
}
const wrapperFunctionTemplate = template(`
function WRAPPER_FUNCTION_ID(ID_PARAM) { function WRAPPER_FUNCTION_ID(ID_PARAM) {
return function(COMPONENT_PARAM) { return function(COMPONENT_PARAM) {
return EXPRESSION; return EXPRESSION;
@ -100,342 +99,334 @@ module.exports = {
} }
`); `);
const VISITED_KEY = 'react-transform-' + Date.now(); const VISITED_KEY = 'react-transform-' + Date.now();
const componentVisitor = { const componentVisitor = {
Class(path) { Class(path) {
if ( if (
path.node[VISITED_KEY] || path.node[VISITED_KEY] ||
!matchesPatterns(path.get('superClass'), this.superClasses) || !matchesPatterns(path.get('superClass'), this.superClasses) ||
!isReactLikeClass(path.node) !isReactLikeClass(path.node)
) { ) {
return; return;
} }
path.node[VISITED_KEY] = true; path.node[VISITED_KEY] = true;
const componentName = (path.node.id && path.node.id.name) || null; const componentName = (path.node.id && path.node.id.name) || null;
const componentId = const componentId = componentName || path.scope.generateUid('component');
componentName || path.scope.generateUid('component'); const isInFunction = hasParentFunction(path);
const isInFunction = hasParentFunction(path);
this.components.push({ this.components.push({
id: componentId, id: componentId,
name: componentName, name: componentName,
isInFunction: isInFunction, isInFunction: isInFunction,
}); });
// Can't wrap ClassDeclarations // Can't wrap ClassDeclarations
const isStatement = t.isStatement(path.node); const isStatement = t.isStatement(path.node);
const isExport = t.isExportDefaultDeclaration(path.parent); const isExport = t.isExportDefaultDeclaration(path.parent);
if (isStatement && !isExport) { if (isStatement && !isExport) {
// class decl // class decl
// need to work around Babel 7 detecting duplicate decls here // need to work around Babel 7 detecting duplicate decls here
path.insertAfter( path.insertAfter(
t.expressionStatement( t.expressionStatement(
t.assignmentExpression( t.assignmentExpression(
'=', '=',
t.identifier(componentId),
wrapComponent(
t.identifier(componentId), t.identifier(componentId),
wrapComponent( componentId,
t.identifier(componentId), this.wrapperFunctionId,
componentId,
this.wrapperFunctionId,
),
), ),
), ),
);
return;
}
const expression = t.toExpression(path.node);
// wrapperFunction("componentId")(node)
let wrapped = wrapComponent(
expression,
componentId,
this.wrapperFunctionId,
);
let constId;
if (isStatement) {
// wrapperFunction("componentId")(class Foo ...) => const Foo = wrapperFunction("componentId")(class Foo ...)
constId = t.identifier(componentName || componentId);
wrapped = t.variableDeclaration('const', [
t.variableDeclarator(constId, wrapped),
]);
}
if (isExport) {
path.parentPath.insertBefore(wrapped);
path.parent.declaration = constId;
} else {
path.replaceWith(wrapped);
}
},
CallExpression(path) {
if (
path.node[VISITED_KEY] ||
!matchesPatterns(path.get('callee'), this.factoryMethods) ||
!isReactLikeComponentObject(path.node.arguments[0])
) {
return;
}
path.node[VISITED_KEY] = true;
// `foo({ displayName: 'NAME' });` => 'NAME'
const componentName = getDisplayName(path.node);
const componentId =
componentName || path.scope.generateUid('component');
const isInFunction = hasParentFunction(path);
this.components.push({
id: componentId,
name: componentName,
isInFunction: isInFunction,
});
path.replaceWith(
wrapComponent(path.node, componentId, this.wrapperFunctionId),
);
},
};
class ReactTransformBuilder {
constructor(file, options) {
this.file = file;
this.program = file.path;
this.options = this.normalizeOptions(options);
// @todo: clean this shit up
this.configuredTransformsIds = [];
}
static validateOptions(options) {
return typeof options === 'object' && Array.isArray(options.transforms);
}
static assertValidOptions(options) {
if (!ReactTransformBuilder.validateOptions(options)) {
throw new Error(
'babel-plugin-react-transform requires that you specify options ' +
'in .babelrc or from the Babel Node API, and that it is an object ' +
'with a transforms property which is an array.',
);
}
}
normalizeOptions(options) {
return {
factoryMethods: options.factoryMethods || ['React.createClass'],
superClasses: options.superClasses || [
'React.Component',
'React.PureComponent',
'Component',
'PureComponent',
],
transforms: options.transforms.map(opts => {
return {
transform: opts.transform,
locals: opts.locals || [],
imports: opts.imports || [],
};
}),
};
}
build(path) {
const componentsDeclarationId = this.file.scope.generateUidIdentifier(
'components',
);
const wrapperFunctionId = this.file.scope.generateUidIdentifier(
'wrapComponent',
);
const components = this.collectAndWrapComponents(wrapperFunctionId);
if (!components.length) {
return;
}
const componentsDeclaration = this.initComponentsDeclaration(
componentsDeclarationId,
components,
);
const configuredTransforms = this.initTransformers(
path,
componentsDeclarationId,
);
const wrapperFunction = this.initWrapperFunction(wrapperFunctionId);
const body = this.program.node.body;
body.unshift(wrapperFunction);
configuredTransforms.reverse().forEach(node => body.unshift(node));
body.unshift(componentsDeclaration);
}
/**
* const Foo = _wrapComponent('Foo')(class Foo extends React.Component {});
* ...
* const Bar = _wrapComponent('Bar')(React.createClass({
* displayName: 'Bar'
* }));
*/
collectAndWrapComponents(wrapperFunctionId) {
const components = [];
this.file.path.traverse(componentVisitor, {
wrapperFunctionId: wrapperFunctionId,
components: components,
factoryMethods: this.options.factoryMethods,
superClasses: this.options.superClasses,
currentlyInFunction: false,
});
return components;
}
/**
* const _components = {
* Foo: {
* displayName: "Foo"
* }
* };
*/
initComponentsDeclaration(componentsDeclarationId, components) {
const props = components.map(component => {
const componentId = component.id;
const componentProps = [];
if (component.name) {
componentProps.push(
t.objectProperty(
t.identifier('displayName'),
t.stringLiteral(component.name),
),
);
}
if (component.isInFunction) {
componentProps.push(
t.objectProperty(
t.identifier('isInFunction'),
t.booleanLiteral(true),
),
);
}
let objectKey;
if (t.isValidIdentifier(componentId)) {
objectKey = t.identifier(componentId);
} else {
objectKey = t.stringLiteral(componentId);
}
return t.objectProperty(
objectKey,
t.objectExpression(componentProps),
);
});
return t.variableDeclaration('const', [
t.variableDeclarator(
componentsDeclarationId,
t.objectExpression(props),
), ),
);
return;
}
const expression = t.toExpression(path.node);
// wrapperFunction("componentId")(node)
let wrapped = wrapComponent(
expression,
componentId,
this.wrapperFunctionId,
);
let constId;
if (isStatement) {
// wrapperFunction("componentId")(class Foo ...) => const Foo = wrapperFunction("componentId")(class Foo ...)
constId = t.identifier(componentName || componentId);
wrapped = t.variableDeclaration('const', [
t.variableDeclarator(constId, wrapped),
]); ]);
} }
/** if (isExport) {
* import _transformLib from "transform-lib"; path.parentPath.insertBefore(wrapped);
* ... path.parent.declaration = constId;
* const _transformLib2 = _transformLib({ } else {
* filename: "filename", path.replaceWith(wrapped);
* components: _components, }
* locals: [], },
* imports: []
* });
*/
initTransformers(path, componentsDeclarationId) {
return this.options.transforms.map(transform => {
const transformName = transform.transform;
const transformImportId = addDefault(path, transformName, {
nameHint: transformName,
});
const transformLocals = transform.locals.map(local => { CallExpression(path) {
return t.identifier(local); if (
}); path.node[VISITED_KEY] ||
!matchesPatterns(path.get('callee'), this.factoryMethods) ||
const transformImports = transform.imports.map(importName => { !isReactLikeComponentObject(path.node.arguments[0])
return addDefault(path, importName, {hint: importName}); ) {
}); return;
const configuredTransformId = this.file.scope.generateUidIdentifier(
transformName,
);
const configuredTransform = t.variableDeclaration('const', [
t.variableDeclarator(
configuredTransformId,
t.callExpression(transformImportId, [
toObjectExpression({
filename: t.stringLiteral(
this.file.opts.filename || 'unknown',
),
components: componentsDeclarationId,
locals: t.arrayExpression(transformLocals),
imports: t.arrayExpression(transformImports),
}),
]),
),
]);
this.configuredTransformsIds.push(configuredTransformId);
return configuredTransform;
});
} }
/** path.node[VISITED_KEY] = true;
* function _wrapComponent(id) {
* return function (Component) {
* return _transformLib2(Component, id);
* };
* }
*/
initWrapperFunction(wrapperFunctionId) {
const idParam = t.identifier('id');
const componentParam = t.identifier('Component');
const expression = this.configuredTransformsIds // `foo({ displayName: 'NAME' });` => 'NAME'
.reverse() const componentName = getDisplayName(path.node);
.reduce((memo, transformId) => { const componentId = componentName || path.scope.generateUid('component');
return t.callExpression(transformId, [memo, idParam]); const isInFunction = hasParentFunction(path);
}, componentParam);
return wrapperFunctionTemplate({ this.components.push({
WRAPPER_FUNCTION_ID: wrapperFunctionId, id: componentId,
ID_PARAM: idParam, name: componentName,
COMPONENT_PARAM: componentParam, isInFunction: isInFunction,
EXPRESSION: expression, });
});
path.replaceWith(
wrapComponent(path.node, componentId, this.wrapperFunctionId),
);
},
};
class ReactTransformBuilder {
constructor(file, options) {
this.file = file;
this.program = file.path;
this.options = this.normalizeOptions(options);
// @todo: clean this shit up
this.configuredTransformsIds = [];
}
static validateOptions(options) {
return typeof options === 'object' && Array.isArray(options.transforms);
}
static assertValidOptions(options) {
if (!ReactTransformBuilder.validateOptions(options)) {
throw new Error(
'babel-plugin-react-transform requires that you specify options ' +
'in .babelrc or from the Babel Node API, and that it is an object ' +
'with a transforms property which is an array.',
);
} }
} }
return { normalizeOptions(options) {
visitor: { return {
Program(path, {file, opts}) { factoryMethods: options.factoryMethods || ['React.createClass'],
ReactTransformBuilder.assertValidOptions(opts); superClasses: options.superClasses || [
const builder = new ReactTransformBuilder(file, opts); 'React.Component',
builder.build(path); 'React.PureComponent',
}, 'Component',
'PureComponent',
],
transforms: options.transforms.map(opts => {
return {
transform: opts.transform,
locals: opts.locals || [],
imports: opts.imports || [],
};
}),
};
}
build(path) {
const componentsDeclarationId = this.file.scope.generateUidIdentifier(
'components',
);
const wrapperFunctionId = this.file.scope.generateUidIdentifier(
'wrapComponent',
);
const components = this.collectAndWrapComponents(wrapperFunctionId);
if (!components.length) {
return;
}
const componentsDeclaration = this.initComponentsDeclaration(
componentsDeclarationId,
components,
);
const configuredTransforms = this.initTransformers(
path,
componentsDeclarationId,
);
const wrapperFunction = this.initWrapperFunction(wrapperFunctionId);
const body = this.program.node.body;
body.unshift(wrapperFunction);
configuredTransforms.reverse().forEach(node => body.unshift(node));
body.unshift(componentsDeclaration);
}
/**
* const Foo = _wrapComponent('Foo')(class Foo extends React.Component {});
* ...
* const Bar = _wrapComponent('Bar')(React.createClass({
* displayName: 'Bar'
* }));
*/
collectAndWrapComponents(wrapperFunctionId) {
const components = [];
this.file.path.traverse(componentVisitor, {
wrapperFunctionId: wrapperFunctionId,
components: components,
factoryMethods: this.options.factoryMethods,
superClasses: this.options.superClasses,
currentlyInFunction: false,
});
return components;
}
/**
* const _components = {
* Foo: {
* displayName: "Foo"
* }
* };
*/
initComponentsDeclaration(componentsDeclarationId, components) {
const props = components.map(component => {
const componentId = component.id;
const componentProps = [];
if (component.name) {
componentProps.push(
t.objectProperty(
t.identifier('displayName'),
t.stringLiteral(component.name),
),
);
}
if (component.isInFunction) {
componentProps.push(
t.objectProperty(
t.identifier('isInFunction'),
t.booleanLiteral(true),
),
);
}
let objectKey;
if (t.isValidIdentifier(componentId)) {
objectKey = t.identifier(componentId);
} else {
objectKey = t.stringLiteral(componentId);
}
return t.objectProperty(objectKey, t.objectExpression(componentProps));
});
return t.variableDeclaration('const', [
t.variableDeclarator(
componentsDeclarationId,
t.objectExpression(props),
),
]);
}
/**
* import _transformLib from "transform-lib";
* ...
* const _transformLib2 = _transformLib({
* filename: "filename",
* components: _components,
* locals: [],
* imports: []
* });
*/
initTransformers(path, componentsDeclarationId) {
return this.options.transforms.map(transform => {
const transformName = transform.transform;
const transformImportId = addDefault(path, transformName, {
nameHint: transformName,
});
const transformLocals = transform.locals.map(local => {
return t.identifier(local);
});
const transformImports = transform.imports.map(importName => {
return addDefault(path, importName, {hint: importName});
});
const configuredTransformId = this.file.scope.generateUidIdentifier(
transformName,
);
const configuredTransform = t.variableDeclaration('const', [
t.variableDeclarator(
configuredTransformId,
t.callExpression(transformImportId, [
toObjectExpression({
filename: t.stringLiteral(this.file.opts.filename || 'unknown'),
components: componentsDeclarationId,
locals: t.arrayExpression(transformLocals),
imports: t.arrayExpression(transformImports),
}),
]),
),
]);
this.configuredTransformsIds.push(configuredTransformId);
return configuredTransform;
});
}
/**
* function _wrapComponent(id) {
* return function (Component) {
* return _transformLib2(Component, id);
* };
* }
*/
initWrapperFunction(wrapperFunctionId) {
const idParam = t.identifier('id');
const componentParam = t.identifier('Component');
const expression = this.configuredTransformsIds
.reverse()
.reduce((memo, transformId) => {
return t.callExpression(transformId, [memo, idParam]);
}, componentParam);
return wrapperFunctionTemplate({
WRAPPER_FUNCTION_ID: wrapperFunctionId,
ID_PARAM: idParam,
COMPONENT_PARAM: componentParam,
EXPRESSION: expression,
});
}
}
return {
visitor: {
Program(path, {file, opts}) {
ReactTransformBuilder.assertValidOptions(opts);
const builder = new ReactTransformBuilder(file, opts);
builder.build(path);
}, },
}; },
}, };
}; };

View File

@ -135,7 +135,7 @@ function makeMakeHMRConfig7() {
return { return {
plugins: [ plugins: [
[ [
require('metro-babel7-plugin-react-transform').default, require('metro-babel7-plugin-react-transform'),
{ {
transforms: [ transforms: [
{ {