@helpscout/hsds-react
Version:
React component library for Help Scout's Design System
237 lines (201 loc) • 7.05 kB
JavaScript
import { API, FileInfo } from 'jscodeshift'
/**
*
* @param {FileInfo} fileInfo
* @param {API} api
* @returns
*/
export default function buttonStandardizationTransform(fileInfo, api) {
const j = api.jscodeshift
const root = j(fileInfo.source)
const isCss = fileInfo.path.endsWith('.css.js')
if (
fileInfo.path.endsWith('.test.js') ||
fileInfo.path.includes('.stories') ||
fileInfo.path.includes('components/Button/Button.jsx') ||
fileInfo.path.includes('components/Button/Button.css.js')
)
return root.toSource()
const processClassnames = p => {
const { node } = p
;[
{ before: 'is-default', after: 'is-default-to-refactor' },
{ before: 'is-xxl', after: 'is-size-xxl' },
{ before: 'is-xl', after: 'is-size-xl' },
{ before: 'is-lg', after: 'is-size-lg' },
{ before: 'is-md', after: 'is-size-md' },
{ before: 'is-sm', after: 'is-size-sm' },
{ before: 'is-xs', after: 'is-size-xs' },
{ before: 'is-xxs', after: 'is-size-xxs' },
{ before: 'is-primary', after: 'is-theme-blue' },
{ before: 'is-secondary', after: 'is-theme-grey' },
{ before: 'is-success', after: 'is-theme-green' },
{ before: 'is-danger', after: 'is-theme-red' },
].forEach(({ before, after }) => {
const quasis = node.quasis.map(q =>
j.templateElement(
{
cooked: q.value.cooked.replace(before, after),
raw: q.value.raw.replace(before, after),
},
false
)
)
node.quasis = quasis
})
return node
}
const getAttribute = (attributes, name) => {
const attr = attributes.find(a => {
if (!a || !a.name || !a.name.name) return null
return a.name.name === name
})
if (!attr) return { value: { value: null, expression: null } }
return { name: attr.name.name, value: attr.value }
}
const filterAttributes = (attributes, extraAttrsToRemoves = []) => {
const toRemove = [
'allowContentEventPropagation',
'disableOnLoading',
'fetch',
'spinButtonOnLoading',
'canRenderFocus',
'isBlock',
...extraAttrsToRemoves,
]
return attributes.filter(a => {
if (!a || !a.name || !a.name.name) return true
return !toRemove.includes(a.name.name)
})
}
const createAttribute = (name, value) => {
if (value === true) {
return j.jsxAttribute(j.jsxIdentifier(name), null)
} else if (value === false) {
j.jsxAttribute(
j.jsxIdentifier(name),
j.jsxExpressionContainer({
type: 'Literal',
value: false,
})
)
}
if (typeof value === 'string') {
return j.jsxAttribute(j.jsxIdentifier(name), j.literal(value))
}
return j.jsxAttribute(j.jsxIdentifier(name), value)
}
const renameAttribute = (attributes, before, after) => {
return attributes.map(a => {
if (a && a.name && a.name.name && a.name.name === before) {
a.name.name = after
}
return a
})
}
const processAttributes = jSXElement => {
const { attributes } = jSXElement
const { value: kind } = getAttribute(attributes, 'kind')
const { value: state } = getAttribute(attributes, 'state')
const { value: size } = getAttribute(attributes, 'size')
const { value: shape } = getAttribute(attributes, 'shape')
const { value: theme } = getAttribute(attributes, 'theme')
const toValidateAttr = createAttribute('data-button-tovalidate', true)
const themeDefaultAttr = createAttribute('theme', 'blue')
const sizeDefaultAttr = createAttribute('size', 'lg')
const outlinedAttr = createAttribute('outlined', true)
const roundedAttr = createAttribute('rounded', true)
const linkedAttr = createAttribute('linked', true)
const extraAttrsToRemoves = [
'kind',
'size',
'state',
'shape',
'withCaret',
'isBorderless',
'iconSize',
]
// if theme exists, do nothing
if (theme.value) return attributes
// if there is no attribute at all, we'll add a data-tovalidate to list the button as a change and let developer decided what they need to do
if (!kind.value && !state.value && !size.value && !size.expression) {
return [
toValidateAttr,
themeDefaultAttr,
sizeDefaultAttr,
linkedAttr,
...filterAttributes(attributes, extraAttrsToRemoves),
]
}
const nextAttributes = []
if (size.value === 'xl' || size.value === 'lgxl') {
nextAttributes.push(createAttribute('size', 'xxl'))
} else if (size.value || size.expression) {
nextAttributes.push(createAttribute('size', size))
}
if (state.value === 'danger') {
nextAttributes.push(createAttribute('theme', 'red'))
}
if (state.value === 'success' || kind.value === 'tertiary') {
nextAttributes.push(createAttribute('theme', 'green'))
}
if (state.value === 'grey') {
nextAttributes.push(createAttribute('theme', 'grey'))
}
if (kind.value === 'primary' && !state.value) {
nextAttributes.push(createAttribute('theme', 'blue'))
}
if (kind.value === 'secondary' && !state.value) {
nextAttributes.push(createAttribute('theme', 'grey'))
}
if (kind.value === 'link') {
nextAttributes.push(createAttribute('theme', 'grey'))
nextAttributes.push(linkedAttr)
}
if (kind.value === 'secondary' || kind.value === 'tertiary') {
nextAttributes.push(outlinedAttr)
}
if (shape.value === 'rounded') {
nextAttributes.push(roundedAttr)
}
if (!kind.value || kind.value === 'default') {
nextAttributes.push(linkedAttr)
}
let updatedAttributes = renameAttribute(attributes, 'innerRef', 'ref')
updatedAttributes = renameAttribute(attributes, 'buttonRef', 'ref')
updatedAttributes = renameAttribute(attributes, 'isLoading', 'loading')
return [
...nextAttributes,
...filterAttributes(updatedAttributes, extraAttrsToRemoves),
]
}
const processButtonComponent = p => {
const jSXElement = p.value
const isIcon = jSXElement.name.name.includes('Icon')
// jSXElement.attributes = jSXElement.attributes.filter(a => {
// if (a.type === 'JSXSpreadAttribute') return true
// return a && a.name && a.name.name !== 'version'
// })
jSXElement.attributes = processAttributes(jSXElement)
}
//replace classNames
const cssElements = root.find(j.TemplateLiteral).filter(p => {
const hasVariable =
p.value.quasis.length > 0 &&
p.value.quasis.some(n => {
const rawString = n.value.raw.toString()
return rawString.includes('Button')
})
return hasVariable
})
cssElements.replaceWith(processClassnames)
// replace jsx component
const elements = root.find(j.JSXOpeningElement).filter(el => {
if (el.node.name.name) {
return el.node.name.name.includes('Button')
}
return false
})
elements.forEach(processButtonComponent)
return root.toSource()
}