Skip to content

Instantly share code, notes, and snippets.

@Sinewyk
Created March 21, 2016 10:01
Show Gist options
  • Save Sinewyk/4ba5fbfc30c04d184de9 to your computer and use it in GitHub Desktop.
Save Sinewyk/4ba5fbfc30c04d184de9 to your computer and use it in GitHub Desktop.
from mixin to HoC for i18n
const path = require('path');
const printOptions = {
quote: 'single',
trailingComma: true,
};
const I18N_MIXIN = 'i18nMixin';
const I18N_MIXIN_FILE = 'mixins/i18n';
const CONNECT_TO_I18N = 'connectToI18n';
module.exports = function transform(file, api) {
const fileName = path.parse(file.path).name;
console.log(fileName);
const source = file.source;
const j = api.jscodeshift;
const root = j(source);
const ReactUtils = require('./utils/ReactUtils')(j);
function shouldProcessFile() {
return root.find(j.ImportDeclaration, {
source: { value: I18N_MIXIN_FILE},
}).size();
}
function isI18nMixin(node) {
return node.type === 'Identifier' &&
node.name === I18N_MIXIN;
}
function hasI18nMixin(classPath) {
const spec = ReactUtils.getReactCreateClassSpec(classPath);
const mixin = spec && spec.properties.find(ReactUtils.isMixinProperty);
return mixin && mixin.value.elements.some(isI18nMixin);
}
function removeI18nMixinFromMixinProperty(elements) {
return j.property(
'init',
j.identifier('mixins'),
j.arrayExpression(
elements.filter(element => !isI18nMixin(element))
)
);
}
function isPropTypesProperty(property) {
const key = property.key;
const value = property.value;
return key.name === 'propTypes' &&
value.type === 'ObjectExpression' &&
Array.isArray(value.properties) &&
value.properties.length;
}
function removeI18nMixinFromClass(classPath) {
const spec = ReactUtils.getReactCreateClassSpec(classPath);
const properties = spec.properties
.map(property => {
if (!ReactUtils.isMixinProperty(property)) {
return property;
}
const elements = property.value.elements;
return elements.length !== 1 ? removeI18nMixinFromMixinProperty(elements) : null;
})
.filter(property => !!property);
ReactUtils.findReactCreateClassCallExpression(classPath).replaceWith(
ReactUtils.createCreateReactClassCallExpression(properties)
);
}
function transformToVariableDeclaration(classPath) {
const reactCallExpression = classPath.value.declaration;
const variableIdentifier = j.identifier(fileName);
const variableDeclaration = j.variableDeclaration('const', [
j.variableDeclarator(
variableIdentifier,
reactCallExpression
),
]);
classPath.replace(variableDeclaration);
}
function exportContainerWithI18n(classPath) {
const exportDeclaration = j.exportDefaultDeclaration(
j.callExpression(
j.identifier(CONNECT_TO_I18N), [
j.identifier(fileName),
]
)
);
classPath.insertAfter(exportDeclaration);
}
function convertToProps(transMethod) {
return j.memberExpression(
j.memberExpression(
j.identifier('this'),
j.identifier('props')
),
j.identifier(transMethod)
);
}
function convertTransToProps(transMethod) {
// Maybe do better than root
return root
.find(j.MemberExpression, {
object: {
type: 'ThisExpression',
},
property: {
name: transMethod,
},
})
.replaceWith(convertToProps(transMethod))
.size();
}
function convertContextToProps() {
return root
.find(j.MemberExpression, {
object: {
type: 'ThisExpression',
},
property: {
name: 'context',
},
})
.replaceWith(
j.memberExpression(
j.identifier('this'),
j.identifier('props')
)
).size();
}
function createProperty(propName, propType) {
const propTypesImported = root.find(j.ImportSpecifier)
.filter(p => p.value.local.name === 'PropTypes')
.size() > 0;
let propTypesMember;
if (propTypesImported) {
propTypesMember = j.identifier('PropTypes');
} else {
propTypesMember = j.memberExpression(
j.identifier('React'),
j.identifier('PropTypes')
);
}
return j.property(
'init',
j.identifier(propName),
j.memberExpression(
j.memberExpression(
propTypesMember,
j.identifier(propType)
),
j.identifier('isRequired')
)
);
}
function usesProp(propName) {
const propsExpressionsForPropName = root
.find(j.MemberExpression, {
object: {
type: 'MemberExpression',
},
property: {
name: propName,
},
})
.filter(p => {
const memberExpression = p.value.object;
return memberExpression.object.name === 'this'
&& memberExpression.property.name === 'props';
});
return propsExpressionsForPropName.size() > 0;
}
function addMethodsToPropTypes(classPath) {
const spec = ReactUtils.getReactCreateClassSpec(classPath);
const properties = spec.properties
.map(property => {
if (!isPropTypesProperty(property)) {
return property;
}
const propTypes = property.value.properties;
if (usesProp('trans')) {
propTypes.push(createProperty('trans', 'func'));
}
if (usesProp('transChoice')) {
propTypes.push(createProperty('transChoice', 'func'));
}
if (usesProp('i18n')) {
propTypes.push(createProperty('i18n', 'string'));
}
return property;
})
.filter(property => !!property);
ReactUtils.findReactCreateClassCallExpression(classPath).replaceWith(
ReactUtils.createCreateReactClassCallExpression(properties)
);
}
if (!shouldProcessFile()) {
return source;
}
const reactClass = ReactUtils.findReactCreateClassExportDefault(root);
const classesWithI18nMixin = reactClass.filter(hasI18nMixin);
const classesWithTransUsage = classesWithI18nMixin.filter(() => {
const numberOfTransChanges = convertTransToProps('trans');
const numberOfTransChoiceChanges = convertTransToProps('transChoice');
const numberOfContextChanges = convertContextToProps();
return numberOfTransChanges > 0 ||
numberOfTransChoiceChanges > 0 ||
numberOfContextChanges > 0;
});
classesWithI18nMixin
.forEach(removeI18nMixinFromClass);
classesWithTransUsage
.forEach(addMethodsToPropTypes)
.forEach(transformToVariableDeclaration)
.forEach(exportContainerWithI18n);
// Change imports
root.find(j.ImportDeclaration, {
source: { value: I18N_MIXIN_FILE},
}).replaceWith(() => {
if (classesWithTransUsage.size() === 0) {
return null;
}
return j.importDeclaration(
[j.importDefaultSpecifier(j.identifier(CONNECT_TO_I18N))],
j.literal('modules/i18n/connectToI18n')
);
});
return root.toSource(printOptions);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment