metro-babel7-plugin-react-transform
Advanced tools
Comparing version 0.33.0 to 0.34.0
@@ -21,3 +21,3 @@ /** | ||
const {transformSync} = require('@babel/core'); | ||
const reactPlugin = require('../lib/index.js').default; | ||
const reactPlugin = require('../lib/index.js'); | ||
@@ -24,0 +24,0 @@ describe('finds React components', () => { |
669
lib/index.js
@@ -31,67 +31,66 @@ /** | ||
module.exports = { | ||
default: function({types: t, template}) { | ||
function matchesPatterns(path, patterns) { | ||
return !!find(patterns, pattern => { | ||
return ( | ||
t.isIdentifier(path.node, {name: pattern}) || | ||
path.matchesPattern(pattern) | ||
); | ||
}); | ||
} | ||
module.exports = function({types: t, template}) { | ||
function matchesPatterns(path, patterns) { | ||
return !!find(patterns, pattern => { | ||
return ( | ||
t.isIdentifier(path.node, {name: pattern}) || | ||
path.matchesPattern(pattern) | ||
); | ||
}); | ||
} | ||
function isReactLikeClass(node) { | ||
return !!find(node.body.body, classMember => { | ||
function isReactLikeClass(node) { | ||
return !!find(node.body.body, classMember => { | ||
return ( | ||
t.isClassMethod(classMember) && | ||
t.isIdentifier(classMember.key, {name: 'render'}) | ||
); | ||
}); | ||
} | ||
function isReactLikeComponentObject(node) { | ||
return ( | ||
t.isObjectExpression(node) && | ||
!!find(node.properties, objectMember => { | ||
return ( | ||
t.isClassMethod(classMember) && | ||
t.isIdentifier(classMember.key, {name: 'render'}) | ||
(t.isObjectProperty(objectMember) || | ||
t.isObjectMethod(objectMember)) && | ||
(t.isIdentifier(objectMember.key, {name: 'render'}) || | ||
t.isStringLiteral(objectMember.key, {value: 'render'})) | ||
); | ||
}); | ||
} | ||
}) | ||
); | ||
} | ||
function isReactLikeComponentObject(node) { | ||
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'})) | ||
); | ||
}) | ||
); | ||
} | ||
// `foo({ displayName: 'NAME' });` => 'NAME' | ||
function getDisplayName(node) { | ||
const property = find( | ||
node.arguments[0].properties, | ||
_node => _node.key.name === 'displayName', | ||
); | ||
return property && property.value.value; | ||
} | ||
// `foo({ displayName: 'NAME' });` => 'NAME' | ||
function getDisplayName(node) { | ||
const property = find( | ||
node.arguments[0].properties, | ||
_node => _node.key.name === 'displayName', | ||
); | ||
return property && property.value.value; | ||
} | ||
function hasParentFunction(path) { | ||
return !!path.findParent(parentPath => parentPath.isFunction()); | ||
} | ||
function hasParentFunction(path) { | ||
return !!path.findParent(parentPath => parentPath.isFunction()); | ||
} | ||
// wrapperFunction("componentId")(node) | ||
function wrapComponent(node, componentId, wrapperFunctionId) { | ||
return t.callExpression( | ||
t.callExpression(wrapperFunctionId, [t.stringLiteral(componentId)]), | ||
[node], | ||
); | ||
} | ||
// wrapperFunction("componentId")(node) | ||
function wrapComponent(node, componentId, wrapperFunctionId) { | ||
return t.callExpression( | ||
t.callExpression(wrapperFunctionId, [t.stringLiteral(componentId)]), | ||
[node], | ||
); | ||
} | ||
// `{ name: foo }` => Node { type: "ObjectExpression", properties: [...] } | ||
function toObjectExpression(object) { | ||
const properties = Object.keys(object).map(key => { | ||
return t.objectProperty(t.identifier(key), object[key]); | ||
}); | ||
// `{ 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); | ||
} | ||
return t.objectExpression(properties); | ||
} | ||
const wrapperFunctionTemplate = template(` | ||
const wrapperFunctionTemplate = template(` | ||
function WRAPPER_FUNCTION_ID(ID_PARAM) { | ||
@@ -104,340 +103,332 @@ return function(COMPONENT_PARAM) { | ||
const VISITED_KEY = 'react-transform-' + Date.now(); | ||
const VISITED_KEY = 'react-transform-' + Date.now(); | ||
const componentVisitor = { | ||
Class(path) { | ||
if ( | ||
path.node[VISITED_KEY] || | ||
!matchesPatterns(path.get('superClass'), this.superClasses) || | ||
!isReactLikeClass(path.node) | ||
) { | ||
return; | ||
} | ||
const componentVisitor = { | ||
Class(path) { | ||
if ( | ||
path.node[VISITED_KEY] || | ||
!matchesPatterns(path.get('superClass'), this.superClasses) || | ||
!isReactLikeClass(path.node) | ||
) { | ||
return; | ||
} | ||
path.node[VISITED_KEY] = true; | ||
path.node[VISITED_KEY] = true; | ||
const componentName = (path.node.id && path.node.id.name) || null; | ||
const componentId = | ||
componentName || path.scope.generateUid('component'); | ||
const isInFunction = hasParentFunction(path); | ||
const componentName = (path.node.id && path.node.id.name) || null; | ||
const componentId = componentName || path.scope.generateUid('component'); | ||
const isInFunction = hasParentFunction(path); | ||
this.components.push({ | ||
id: componentId, | ||
name: componentName, | ||
isInFunction: isInFunction, | ||
}); | ||
this.components.push({ | ||
id: componentId, | ||
name: componentName, | ||
isInFunction: isInFunction, | ||
}); | ||
// Can't wrap ClassDeclarations | ||
const isStatement = t.isStatement(path.node); | ||
const isExport = t.isExportDefaultDeclaration(path.parent); | ||
// Can't wrap ClassDeclarations | ||
const isStatement = t.isStatement(path.node); | ||
const isExport = t.isExportDefaultDeclaration(path.parent); | ||
if (isStatement && !isExport) { | ||
// class decl | ||
// need to work around Babel 7 detecting duplicate decls here | ||
if (isStatement && !isExport) { | ||
// class decl | ||
// need to work around Babel 7 detecting duplicate decls here | ||
path.insertAfter( | ||
t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
path.insertAfter( | ||
t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
t.identifier(componentId), | ||
wrapComponent( | ||
t.identifier(componentId), | ||
wrapComponent( | ||
t.identifier(componentId), | ||
componentId, | ||
this.wrapperFunctionId, | ||
), | ||
componentId, | ||
this.wrapperFunctionId, | ||
), | ||
), | ||
); | ||
), | ||
); | ||
return; | ||
} | ||
return; | ||
} | ||
const expression = t.toExpression(path.node); | ||
const expression = t.toExpression(path.node); | ||
// wrapperFunction("componentId")(node) | ||
let wrapped = wrapComponent( | ||
expression, | ||
componentId, | ||
this.wrapperFunctionId, | ||
); | ||
let constId; | ||
// 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 (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); | ||
} | ||
}, | ||
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; | ||
} | ||
CallExpression(path) { | ||
if ( | ||
path.node[VISITED_KEY] || | ||
!matchesPatterns(path.get('callee'), this.factoryMethods) || | ||
!isReactLikeComponentObject(path.node.arguments[0]) | ||
) { | ||
return; | ||
} | ||
path.node[VISITED_KEY] = true; | ||
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); | ||
// `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, | ||
}); | ||
this.components.push({ | ||
id: componentId, | ||
name: componentName, | ||
isInFunction: isInFunction, | ||
}); | ||
path.replaceWith( | ||
wrapComponent(path.node, componentId, this.wrapperFunctionId), | ||
); | ||
}, | ||
}; | ||
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); | ||
class ReactTransformBuilder { | ||
constructor(file, options) { | ||
this.file = file; | ||
this.program = file.path; | ||
this.options = this.normalizeOptions(options); | ||
// @todo: clean this shit up | ||
this.configuredTransformsIds = []; | ||
} | ||
// @todo: clean this shit up | ||
this.configuredTransformsIds = []; | ||
} | ||
static validateOptions(options) { | ||
return typeof options === 'object' && Array.isArray(options.transforms); | ||
} | ||
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.', | ||
); | ||
} | ||
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 || [], | ||
}; | ||
}), | ||
}; | ||
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; | ||
} | ||
build(path) { | ||
const componentsDeclarationId = this.file.scope.generateUidIdentifier( | ||
'components', | ||
); | ||
const wrapperFunctionId = this.file.scope.generateUidIdentifier( | ||
'wrapComponent', | ||
); | ||
const componentsDeclaration = this.initComponentsDeclaration( | ||
componentsDeclarationId, | ||
components, | ||
); | ||
const configuredTransforms = this.initTransformers( | ||
path, | ||
componentsDeclarationId, | ||
); | ||
const wrapperFunction = this.initWrapperFunction(wrapperFunctionId); | ||
const components = this.collectAndWrapComponents(wrapperFunctionId); | ||
const body = this.program.node.body; | ||
if (!components.length) { | ||
return; | ||
} | ||
body.unshift(wrapperFunction); | ||
configuredTransforms.reverse().forEach(node => body.unshift(node)); | ||
body.unshift(componentsDeclaration); | ||
} | ||
const componentsDeclaration = this.initComponentsDeclaration( | ||
componentsDeclarationId, | ||
components, | ||
); | ||
const configuredTransforms = this.initTransformers( | ||
path, | ||
componentsDeclarationId, | ||
); | ||
const wrapperFunction = this.initWrapperFunction(wrapperFunctionId); | ||
/** | ||
* const Foo = _wrapComponent('Foo')(class Foo extends React.Component {}); | ||
* ... | ||
* const Bar = _wrapComponent('Bar')(React.createClass({ | ||
* displayName: 'Bar' | ||
* })); | ||
*/ | ||
collectAndWrapComponents(wrapperFunctionId) { | ||
const components = []; | ||
const body = this.program.node.body; | ||
this.file.path.traverse(componentVisitor, { | ||
wrapperFunctionId: wrapperFunctionId, | ||
components: components, | ||
factoryMethods: this.options.factoryMethods, | ||
superClasses: this.options.superClasses, | ||
currentlyInFunction: false, | ||
}); | ||
body.unshift(wrapperFunction); | ||
configuredTransforms.reverse().forEach(node => body.unshift(node)); | ||
body.unshift(componentsDeclaration); | ||
} | ||
return components; | ||
} | ||
/** | ||
* const Foo = _wrapComponent('Foo')(class Foo extends React.Component {}); | ||
* ... | ||
* const Bar = _wrapComponent('Bar')(React.createClass({ | ||
* displayName: 'Bar' | ||
* })); | ||
*/ | ||
collectAndWrapComponents(wrapperFunctionId) { | ||
const components = []; | ||
/** | ||
* const _components = { | ||
* Foo: { | ||
* displayName: "Foo" | ||
* } | ||
* }; | ||
*/ | ||
initComponentsDeclaration(componentsDeclarationId, components) { | ||
const props = components.map(component => { | ||
const componentId = component.id; | ||
const componentProps = []; | ||
this.file.path.traverse(componentVisitor, { | ||
wrapperFunctionId: wrapperFunctionId, | ||
components: components, | ||
factoryMethods: this.options.factoryMethods, | ||
superClasses: this.options.superClasses, | ||
currentlyInFunction: false, | ||
}); | ||
if (component.name) { | ||
componentProps.push( | ||
t.objectProperty( | ||
t.identifier('displayName'), | ||
t.stringLiteral(component.name), | ||
), | ||
); | ||
} | ||
return components; | ||
} | ||
if (component.isInFunction) { | ||
componentProps.push( | ||
t.objectProperty( | ||
t.identifier('isInFunction'), | ||
t.booleanLiteral(true), | ||
), | ||
); | ||
} | ||
/** | ||
* const _components = { | ||
* Foo: { | ||
* displayName: "Foo" | ||
* } | ||
* }; | ||
*/ | ||
initComponentsDeclaration(componentsDeclarationId, components) { | ||
const props = components.map(component => { | ||
const componentId = component.id; | ||
const componentProps = []; | ||
let objectKey; | ||
if (component.name) { | ||
componentProps.push( | ||
t.objectProperty( | ||
t.identifier('displayName'), | ||
t.stringLiteral(component.name), | ||
), | ||
); | ||
} | ||
if (t.isValidIdentifier(componentId)) { | ||
objectKey = t.identifier(componentId); | ||
} else { | ||
objectKey = t.stringLiteral(componentId); | ||
} | ||
if (component.isInFunction) { | ||
componentProps.push( | ||
t.objectProperty( | ||
t.identifier('isInFunction'), | ||
t.booleanLiteral(true), | ||
), | ||
); | ||
} | ||
return t.objectProperty(objectKey, t.objectExpression(componentProps)); | ||
}); | ||
let objectKey; | ||
return t.variableDeclaration('const', [ | ||
t.variableDeclarator( | ||
componentsDeclarationId, | ||
t.objectExpression(props), | ||
), | ||
]); | ||
} | ||
if (t.isValidIdentifier(componentId)) { | ||
objectKey = t.identifier(componentId); | ||
} else { | ||
objectKey = t.stringLiteral(componentId); | ||
} | ||
/** | ||
* 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, | ||
}); | ||
return t.objectProperty( | ||
objectKey, | ||
t.objectExpression(componentProps), | ||
); | ||
const transformLocals = transform.locals.map(local => { | ||
return t.identifier(local); | ||
}); | ||
return t.variableDeclaration('const', [ | ||
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( | ||
componentsDeclarationId, | ||
t.objectExpression(props), | ||
configuredTransformId, | ||
t.callExpression(transformImportId, [ | ||
toObjectExpression({ | ||
filename: t.stringLiteral(this.file.opts.filename || 'unknown'), | ||
components: componentsDeclarationId, | ||
locals: t.arrayExpression(transformLocals), | ||
imports: t.arrayExpression(transformImports), | ||
}), | ||
]), | ||
), | ||
]); | ||
} | ||
/** | ||
* 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, | ||
}); | ||
this.configuredTransformsIds.push(configuredTransformId); | ||
const transformLocals = transform.locals.map(local => { | ||
return t.identifier(local); | ||
}); | ||
return configuredTransform; | ||
}); | ||
} | ||
const transformImports = transform.imports.map(importName => { | ||
return addDefault(path, importName, {hint: importName}); | ||
}); | ||
/** | ||
* function _wrapComponent(id) { | ||
* return function (Component) { | ||
* return _transformLib2(Component, id); | ||
* }; | ||
* } | ||
*/ | ||
initWrapperFunction(wrapperFunctionId) { | ||
const idParam = t.identifier('id'); | ||
const componentParam = t.identifier('Component'); | ||
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), | ||
}), | ||
]), | ||
), | ||
]); | ||
const expression = this.configuredTransformsIds | ||
.reverse() | ||
.reduce((memo, transformId) => { | ||
return t.callExpression(transformId, [memo, idParam]); | ||
}, componentParam); | ||
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 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); | ||
}, | ||
return { | ||
visitor: { | ||
Program(path, {file, opts}) { | ||
ReactTransformBuilder.assertValidOptions(opts); | ||
const builder = new ReactTransformBuilder(file, opts); | ||
builder.build(path); | ||
}, | ||
}; | ||
}, | ||
}, | ||
}; | ||
}; |
{ | ||
"version": "0.33.0", | ||
"version": "0.34.0", | ||
"name": "metro-babel7-plugin-react-transform", | ||
@@ -4,0 +4,0 @@ "main": "lib/index.js", |
41407
587