ra-core
Version:
Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React
318 lines (291 loc) • 10 kB
text/typescript
import j from 'jscodeshift';
module.exports = (file, api: j.API) => {
const j = api.jscodeshift;
const root = j(file.source);
const continueAfterImport = replaceImport(root, j);
if (!continueAfterImport) {
return root.toSource();
}
const continueAfterReplaceDatagrid = replaceDatagrid(root, j);
if (!continueAfterReplaceDatagrid) {
return root.toSource();
}
transformChildren(root, j);
cleanImports(root, j);
return root.toSource({ quote: 'single', lineTerminator: '\n' });
};
const replaceImport = (root, j) => {
// Check if there is an import from react-admin
const reactAdminImport = root.find(j.ImportDeclaration, {
source: {
value: 'react-admin',
},
});
if (!reactAdminImport.length) {
return false;
}
// Check if there is an import of DataGrid from react-admin
const datagridImport = reactAdminImport.filter(path => {
return path.node.specifiers.some(
specifier =>
j.ImportSpecifier.check(specifier) &&
specifier.imported.name === 'Datagrid'
);
});
if (!datagridImport.length) {
return false;
}
// Replace import of DataGrid with DataTable
reactAdminImport.replaceWith(({ node }) =>
j.importDeclaration(
node.specifiers.map(specifier => {
if (
j.ImportSpecifier.check(specifier) &&
specifier.imported.name === 'Datagrid'
) {
return j.importSpecifier(j.identifier('DataTable'));
}
return specifier;
}),
node.source
)
);
return true;
};
const replaceDatagrid = (root, j) => {
// Find all instances of Datagrid
const datagridComponents = root.find(j.JSXElement, {
openingElement: {
name: {
type: 'JSXIdentifier',
name: 'Datagrid',
},
},
});
if (!datagridComponents.length) {
return false;
}
// Replace Datagrid with DataTable
datagridComponents.replaceWith(({ node }) => {
return {
...node,
openingElement: {
...node.openingElement,
name: j.jsxIdentifier('DataTable'),
attributes: cleanAttributes(node, j),
},
closingElement: {
...node.closingElement,
name: j.jsxIdentifier('DataTable'),
},
};
});
return true;
};
const cleanAttributes = (node, j) => {
const initialAttributes = node.openingElement.attributes;
// rename the `rowStyle` attribute to `rowSx` if it exists
const rowSxRenamedAttributes = initialAttributes.map(attr => {
if (j.JSXAttribute.check(attr) && attr.name.name === 'rowStyle') {
return j.jsxAttribute(j.jsxIdentifier('rowSx'), attr.value);
}
return attr;
});
// rename the keys of the "sx" prop from "& .RaDatagrid-xxxx" to "& .RaDataTable-xxxx"
const sxRenamedAttributes = rowSxRenamedAttributes.map(attr => {
if (
j.JSXAttribute.check(attr) &&
attr.name.name === 'sx' &&
j.JSXExpressionContainer.check(attr.value)
) {
const expression = attr.value.expression;
if (j.ObjectExpression.check(expression)) {
expression.properties.map(prop => {
if (
j.ObjectProperty.check(prop) &&
j.Literal.check(prop.key) &&
typeof prop.key.value === 'string'
) {
prop.key.value = prop.key.value.replace(
/RaDatagrid-/g,
'RaDataTable-'
);
}
return prop;
});
return attr;
}
}
return attr;
});
// remove the `optimized` attribute if it exists
const finalAttributes = sxRenamedAttributes.filter(
attr => !(j.JSXAttribute.check(attr) && attr.name.name === 'optimized')
);
return finalAttributes;
};
const transformChildren = (root, j) => {
// Find all instances of Datagrid
const datagridComponents = root.find(j.JSXElement, {
openingElement: {
name: {
type: 'JSXIdentifier',
name: 'DataTable',
},
},
});
if (!datagridComponents.length) {
return false;
}
// For each DataTable component, wrap its children in DataTable.Col
datagridComponents.forEach(dataTableComponent => {
const children = dataTableComponent.value.children.filter(child =>
j.JSXElement.check(child)
);
children.forEach(child => {
transformChild(root, j, child);
});
});
};
const transformChild = (root, j, child) => {
let newChild;
if (
j.JSXElement.check(child) &&
child.openingElement.name.type === 'JSXIdentifier' &&
child.openingElement.name.name === 'TextField' &&
!child.openingElement.attributes.some(
attr =>
j.JSXAttribute.check(attr) &&
!['source', 'label', 'empty'].includes(attr.name.name)
)
) {
child.openingElement.name.name = 'DataTable.Col';
} else if (
j.JSXElement.check(child) &&
child.openingElement.name.type === 'JSXIdentifier' &&
child.openingElement.name.name === 'NumberField' &&
!child.openingElement.attributes.some(
attr =>
j.JSXAttribute.check(attr) &&
!['source', 'label', 'empty', 'options', 'locales'].includes(
attr.name.name
)
)
) {
child.openingElement.name.name = 'DataTable.NumberCol';
} else {
newChild = wrapChild(j, child);
// Replace the original child with the new child
root.find(j.JSXElement, {
openingElement: {
name: {
type: 'JSXIdentifier',
name: 'DataTable',
},
},
}).forEach(dataTableComponent => {
dataTableComponent.value.children =
dataTableComponent.value.children.map(c =>
c === child ? newChild : c
);
});
}
};
const wrapChild = (j, child) => {
const labelAttribute = child.openingElement.attributes.find(
attr => j.JSXAttribute.check(attr) && attr.name.name === 'label'
);
const sourceAttribute = child.openingElement.attributes.find(
attr => j.JSXAttribute.check(attr) && attr.name.name === 'source'
);
// Wrap the child in a DataTable.Col component
return j.jsxElement(
j.jsxOpeningElement(
j.jsxIdentifier('DataTable.Col'),
labelAttribute
? [labelAttribute]
: sourceAttribute
? [sourceAttribute]
: [],
false
),
j.jsxClosingElement(j.jsxIdentifier('DataTable.Col')),
[j.jsxText('\n'), child, j.jsxText('\n')]
);
};
const cleanImports = (root, j) => {
// Check if there is still a use of TextField in the code
const textFieldUsage = root.find(j.JSXElement, {
openingElement: {
name: {
type: 'JSXIdentifier',
name: 'TextField',
},
},
});
// Check if there is still a use of NumberField in the code
const numberFieldUsage = root.find(j.JSXElement, {
openingElement: {
name: {
type: 'JSXIdentifier',
name: 'NumberField',
},
},
});
const imports = root.find(j.ImportDeclaration, {
source: {
value: 'react-admin',
},
});
// Check if there is an import of TextField from react-admin
const textFieldImport = imports.filter(path => {
return path.node.specifiers.some(
specifier =>
j.ImportSpecifier.check(specifier) &&
specifier.imported.name === 'TextField'
);
});
const numberFieldImport = imports.filter(path => {
return path.node.specifiers.some(
specifier =>
j.ImportSpecifier.check(specifier) &&
specifier.imported.name === 'NumberField'
);
});
if (!textFieldUsage.length && textFieldImport.length) {
// Remove the import of TextField from react-admin
textFieldImport.forEach(path => {
path.node.specifiers = path.node.specifiers.filter(
specifier =>
!(
j.ImportSpecifier.check(specifier) &&
specifier.imported.name === 'TextField'
)
);
});
// Remove the import declaration if there are no more specifiers
root.find(j.ImportDeclaration).forEach(path => {
if (path.node.specifiers.length === 0) {
j(path).remove();
}
});
}
if (!numberFieldUsage.length && numberFieldImport.length) {
// Remove the import of NumberField from react-admin
numberFieldImport.forEach(path => {
path.node.specifiers = path.node.specifiers.filter(
specifier =>
!(
j.ImportSpecifier.check(specifier) &&
specifier.imported.name === 'NumberField'
)
);
});
// Remove the import declaration if there are no more specifiers
root.find(j.ImportDeclaration).forEach(path => {
if (path.node.specifiers.length === 0) {
j(path).remove();
}
});
}
};