codemods/removeSystemProps.js (280 lines of code) (raw):

const prettify = require('./lib/prettify') const COMMON = [ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'marginX', 'marginY', 'm', 'mt', 'mr', 'mb', 'ml', 'mx', 'my', 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'paddingX', 'paddingY', 'p', 'pt', 'pr', 'pb', 'pl', 'px', 'py', 'color', 'backgroundColor', 'opacity', 'bg', 'display' ] const TYPOGRAPHY = [ 'fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'textAlign', 'fontStyle', 'whiteSpace' ] const BORDER = [ 'border', 'borderWidth', 'borderStyle', 'borderColor', 'borderRadius', 'borderTop', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderRight', 'borderBottom', 'borderBottomLeftRadius', 'borderBottomRightRadius', 'borderLeft', 'borderX', 'borderY', 'borderTopWidth', 'borderTopColor', 'borderTopStyle', 'borderBottomWidth', 'borderBottomColor', 'borderBottomStyle', 'borderLeftWidth', 'borderLeftColor', 'borderLeftStyle', 'borderRightWidth', 'borderRightColor', 'borderRightStyle', 'boxShadow', 'textShadow' ] const LAYOUT = [ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight', 'size', 'overflow', 'overflowX', 'overflowY', 'display', 'verticalAlign' ] const POSITION = ['position', 'zIndex', 'top', 'right', 'bottom', 'left'] const FLEX = [ 'alignItems', 'alignContent', 'justifyItems', 'justifyContent', 'flexWrap', 'flexDirection', 'flex', 'flexGrow', 'flexShrink', 'flexBasis', 'justifySelf', 'alignSelf', 'order' ] // const GRID = [ // 'gridGap', // 'gridColumnGap', // 'gridRowGap', // 'gridColumn', // 'gridRow', // 'gridAutoFlow', // 'gridAutoColumns', // 'gridAutoRows', // 'gridTemplateColumns', // 'gridTemplateRows', // 'gridTemplateAreas', // 'gridArea' // ] const stylePropsMap = { Avatar: [...COMMON], AvatarStack: [...COMMON], BranchName: [...COMMON], Breadcrumb: [...COMMON, ...FLEX], Button: [...COMMON, ...LAYOUT, ...TYPOGRAPHY], ButtonBase: [...COMMON, ...LAYOUT], ButtonClose: [...COMMON, ...LAYOUT], ButtonTableList: [...COMMON, ...TYPOGRAPHY, ...LAYOUT], CircleBadge: [...COMMON], CounterLabel: [...COMMON], Details: [...COMMON], Dialog: [...LAYOUT, ...COMMON, ...POSITION], Dropdown: [...COMMON], FilteredSearch: [...COMMON], FilterList: [...COMMON], Flash: [...COMMON], FormGroup: [...COMMON], FormGroupLabel: [...COMMON, ...TYPOGRAPHY], Header: [...COMMON, ...BORDER], HeaderItem: [...COMMON, ...BORDER], Label: [...COMMON, ...BORDER], LabelGroup: [...COMMON], Link: [...COMMON, ...TYPOGRAPHY], Overlay: [...COMMON], Pagehead: [...COMMON], Pagination: [...COMMON], Popover: [...COMMON, ...LAYOUT, ...POSITION], PopoverContent: [...COMMON, ...LAYOUT, ...POSITION, ...FLEX], SelectMenu: [...COMMON], SelectMenuDivider: [...COMMON], SelectMenuFilter: [...COMMON], SelectMenuFooter: [...COMMON], SelectMenuHeader: [...COMMON, ...TYPOGRAPHY], SelectMenuItem: [...COMMON], SelectMenuList: [...COMMON], SelectMenuLoadingAnimation: [...COMMON], SelectMenuModal: [...COMMON], SelectMenuTab: [...COMMON], SelectMenuTabPanel: [...COMMON], SelectMenuTabs: [...COMMON], SideNav: [...COMMON], Spinner: [...COMMON], StateLabel: [...COMMON], StyledOcticon: [...COMMON], SubNav: [...COMMON, ...FLEX], TabNav: [...COMMON], TabNavLink: [...COMMON, ...TYPOGRAPHY], TextInput: [...COMMON], Timeline: [...COMMON], Tooltip: [...COMMON], Truncate: [...TYPOGRAPHY, ...COMMON], UnderlineNav: [...COMMON] } const expressionToString = expression => { if (expression.type === 'Literal') { const expressionValue = expression.value return typeof expressionValue === 'string' ? `"${expressionValue}"` : expressionValue } else if (expression.type === 'Identifier') { return expression.name } else if (expression.type === 'Identifier') { return expression.name } else if (['null', 'undefined'].includes(expression.raw)) { return expression.raw } else { const start = expression.start const end = expression.end const toks = expression.loc.tokens.filter(token => { return token.type !== 'CommentLine' && token.start >= start && token.end <= end }) const vals = toks.map(tok => { return tok.type.label === 'string' ? `"${tok.value}"` : tok.value }) return vals.join('') } } const objectToString = (object, spreads = []) => { const values = Object.values(object) const keys = Object.keys(object) const duples = keys.map(function (key, i) { return [key, values[i]] }) const accumulator = (string, duple) => { const expression = duple[1] const expressionString = expressionToString(expression) return `${string} ${duple[0]}: ${expressionString},` } const objString = duples.reduce(accumulator, '') const spreadsString = spreads.map(s => `...${s},`).join('') return `{${spreadsString}${objString}}` } module.exports = (file, api) => { const j = api.jscodeshift const ast = j(file.source) const importsByName = {} ast .find(j.ImportDeclaration, decl => decl.source.value.includes('@primer/components')) .forEach(decl => { j(decl) .find(j.ImportSpecifier) .forEach(spec => { importsByName[spec.node.imported.name] = spec.node.local.name }) }) ast .find(j.JSXElement, { openingElement: { name: { name: name => { return name in stylePropsMap } } } }) .forEach(el => { const sx = {} const elementName = el.value?.openingElement?.name?.name const elementNameScrubbed = elementName.replace('.', '') const systemProps = stylePropsMap[elementNameScrubbed] const attrNodes = j(el).find(j.JSXAttribute, { name: name => { const isInElement = name.start >= el.node.start && name.end <= el.value.openingElement.end return systemProps && systemProps.includes(name.name) && isInElement } }) const sxNodes = j(el).find(j.JSXAttribute, { name: name => { const isInElement = name.start >= el.node.start && name.end <= el.value.openingElement.end return name.name === 'sx' && isInElement } }) const existingSx = {} const sxNodesArray = sxNodes.nodes() || [] const existingSxProps = sxNodesArray[0]?.value?.expression?.properties existingSxProps && existingSxProps.forEach(p => { const keyName = p?.key?.name const keyValue = p?.key?.raw if (!keyName && !keyValue) { return } existingSx[keyName || keyValue] = p.value }) const spreads = existingSxProps && existingSxProps .filter(p => p.type === 'SpreadElement') .map(s => { const argName = s?.argument?.name const propName = s?.argument?.property?.name const objectName = s?.argument?.object?.name return argName || `${objectName}.${propName}` }) attrNodes.forEach((attr, index) => { const key = attr?.value?.name?.name const literal = attr?.value?.value const val = literal.type === 'JSXExpressionContainer' ? literal.expression : literal if (key && val) { sx[key] = val } if (index + 1 !== attrNodes.length) { attr.prune() } else { const keys = Object.keys(sx) if (keys.length > 0) { sxNodes.forEach(node => node.prune()) j(attr).replaceWith(`sx={${objectToString({...existingSx, ...sx}, spreads)}}`) } } }) }) return prettify(ast, file) }