babel-plugin-transform-react-fela-display-name
Version:
This plugin transforms the display names of all react-fela components created with createComponent or createComponentWithProxy to the name of the variable to which they are assigned.
240 lines (222 loc) • 9.34 kB
JavaScript
const transformReactFelaDisplayName = ({ types: t }) => {
const defaultFunctionNameRegEx = /^createComponent(WithProxy)?$/;
const defaultReactFelaPackageRegEx = /react-fela(\/.*(\.js)?)?$/;
const handleInjectDisplayName = (initialLineNodePath, componentName, objectName) => {
if (!initialLineNodePath || !componentName) return;
const leftLeft = objectName
? t.memberExpression(t.identifier(objectName), t.identifier(componentName))
: t.identifier(componentName);
const left = t.memberExpression(leftLeft, t.identifier('displayName'));
const right = t.stringLiteral(componentName);
const displayNameAssignment = t.toStatement(t.assignmentExpression('=', left, right));
initialLineNodePath.insertAfter(displayNameAssignment);
};
const identifierComesFromReactFela = (
identifierDeclarationPath,
calleeName,
functionNameRegEx,
reactFelaPackageRegEx
) => {
const { scope: { bindings } } = identifierDeclarationPath;
if (!bindings[calleeName]) return false;
const sourcePath = bindings[calleeName].path;
if (sourcePath.isImportSpecifier() && sourcePath.parentPath.isImportDeclaration()) {
// Handle cases where the function is imported destructured. For example:
//
// import { createComponent } from 'react-fela';
// /* or */
// import { createComponentWithProxy }j from 'react-fela';
//
const {
parent: { source: { value: sourceImportFrom } },
node: { imported: { name: importedName } }
} = sourcePath;
const isFromReactFela = reactFelaPackageRegEx.test(sourceImportFrom);
const validImportedName = functionNameRegEx.test(importedName);
return isFromReactFela && validImportedName;
} else if (sourcePath.isVariableDeclarator()) {
const { node: { init } } = sourcePath;
// This handles the following case:
//
// const createComponent = require('react-fela').createComponent;
//
if (t.isMemberExpression(init)) {
const { property, object: { callee } } = init;
return (
callee && callee.name === 'require' && property && functionNameRegEx.test(property.name)
);
}
}
return false;
};
return {
name: 'transform-react-fela-display-name',
visitor: {
AssignmentExpression(path, { opts }) {
// This adds the ability to handle components created and assigned to objects of properties.
// This handles the case of a component being created as a class property. For example:
//
// class MyParentComponent extends React.Component {
// static MyChildComponent = createComponent(() => ({}), 'div');
// ...
// }
//
const { node: { left, right } } = path;
const functionNameRegEx = new RegExp(opts.functionNameRegEx) || defaultFunctionNameRegEx;
const reactFelaPackageRegEx =
new RegExp(opts.reactFelaPackageRegEx) || defaultReactFelaPackageRegEx;
if (t.isMemberExpression(left) && t.isCallExpression(right)) {
const injectAssignmentDisplayName = () => {
const { object: { name: objectName }, property: { name: propertyName } } = left;
return handleInjectDisplayName(path.parentPath, propertyName, objectName);
};
const { callee } = right;
if (t.isMemberExpression(callee)) {
// This handles the case where the assignment is to a default import of the package.
// For example:
//
// import ReactFela from 'react-fela';
// class MyParentComponent extends React.Component {
// static MyChildComponent = ReactFela.createComponent(() => ({}), 'div');
// }
//
const { object: { name: variableName } } = callee;
const { scope: { bindings } } = path;
if (variableName && variableName === opts.globalSource) {
injectAssignmentDisplayName();
return;
}
const binding = bindings[variableName];
if (!binding || !binding.path || !binding.path.parent) return;
const { path: { parent: importDeclaration } } = binding;
if (t.isImportDeclaration(importDeclaration)) {
injectAssignmentDisplayName();
}
} else if (
identifierComesFromReactFela(
path,
callee.name,
functionNameRegEx,
reactFelaPackageRegEx
)
) {
injectAssignmentDisplayName();
}
}
},
VariableDeclarator(path, { opts }) {
// Match cases such as:
//
// const x = y;
//
const { node: { id, init } } = path;
const functionNameRegEx = new RegExp(opts.functionNameRegEx) || defaultFunctionNameRegEx;
const reactFelaPackageRegEx =
new RegExp(opts.reactFelaPackageRegEx) || defaultReactFelaPackageRegEx;
if (!init) return;
const { callee } = init;
if (t.isCallExpression(init)) {
// Match cases such as:
//
// const x = y();
//
const componentName = id.name;
let initialLineNodePath = path.parentPath;
// in case there is an Named Export declaration upstream such as:
// export const MyComponent = createComponent(MyComponentRules, 'div');
// we will want to insert the displayName assignment after the export decleration
const exportNamedDeclaration = initialLineNodePath.findParent(p =>
p.isExportNamedDeclaration()
);
if (exportNamedDeclaration) {
initialLineNodePath = exportNamedDeclaration;
}
const injectDisplayName = () =>
handleInjectDisplayName(initialLineNodePath, componentName);
if (
callee.name &&
callee.name.match(functionNameRegEx) &&
identifierComesFromReactFela(
path,
callee.name,
functionNameRegEx,
reactFelaPackageRegEx
)
) {
// Match cases such as:
//
// const x = createComponent(...);
// /* or */
// const y = createComponentWithProxy(...);
//
injectDisplayName();
} else if (t.isMemberExpression(callee)) {
// This handles default imports of createComponent functions. For example:
//
// import ReactFela from 'react-fela';
// const renameIt = createComponent;
// const MyComponent = renameIt(...);
//
const { object: { name: variableName } } = callee;
if (variableName && variableName === opts.globalSource) {
// This handles the case where the recipient matches the provided global source name.
// For example:
// /* babel plugin options = { globalSource: 'ReactFela' } */
// const MyComponent = ReactFela.createComponent(...);
//
injectDisplayName();
return;
}
const { scope: { bindings } } = path;
if (!bindings[variableName]) return;
const { path: { parent, node: bindingNode } } = bindings[variableName];
if (t.isImportDeclaration(parent)) {
const { path: { parent: { source: { value } } } } = bindings[variableName];
if (reactFelaPackageRegEx.test(value)) {
injectDisplayName();
}
} else if (t.isVariableDeclaration(parent)) {
// This handles the following case:
//
// const ReactFela = require('react-fela');
// const MyComponent = ReactFela.createComponent(...);
//
const { init: bindingInit } = bindingNode;
if (t.isVariableDeclarator(bindingNode) && t.isCallExpression(bindingInit)) {
const isRequiredFromReact = bindingInit.arguments.some(arg =>
reactFelaPackageRegEx.test(arg.value)
);
if (isRequiredFromReact) {
injectDisplayName();
}
}
}
} else {
const { scope: { bindings } } = path;
const functionBinding = bindings[callee.name];
if (!functionBinding) return;
const { path: { node: { init: bindingInit } } } = functionBinding;
if (
t.isIdentifier(bindingInit) &&
identifierComesFromReactFela(
functionBinding.path,
bindingInit.name,
functionNameRegEx,
reactFelaPackageRegEx
)
) {
// This handles renaming of the createComponent functions. For example:
//
// import { createComponent } from 'react-fela';
// const renameIt = createComponent;
// const MyComponent = renameIt(...);
//
injectDisplayName();
}
}
}
}
}
};
};
module.exports = transformReactFelaDisplayName;