1// Based on https://github.com/gregberge/svgr/tree/master/packages/babel-plugin-transform-react-native-svg 2 3const elementToComponent = { 4 a: 'A', 5 article: 'Article', 6 b: 'B', 7 br: 'BR', 8 caption: 'Caption', 9 code: 'Code', 10 div: 'Div', 11 footer: 'Footer', 12 h1: 'H1', 13 h2: 'H2', 14 pre: 'Pre', 15 h3: 'H3', 16 h4: 'H4', 17 h5: 'H5', 18 h6: 'H6', 19 header: 'Header', 20 time: 'Time', 21 hr: 'HR', 22 i: 'I', 23 mark: 'Mark', 24 del: 'Del', 25 em: 'EM', 26 li: 'LI', 27 main: 'Main', 28 nav: 'Nav', 29 p: 'P', 30 s: 'S', 31 section: 'Section', 32 table: 'Table', 33 tbody: 'TBody', 34 td: 'TD', 35 th: 'TH', 36 thead: 'THead', 37 tr: 'TR', 38 ul: 'UL', 39 strong: 'Strong', 40 span: 'Span', 41 aside: 'Aside', 42 tfoot: 'TFoot', 43 blockquote: 'BlockQuote', 44 q: 'Q', 45 46 html: 'Div', 47 body: 'Div', 48 49 // TODO: img 50 // NOTE: head, meta, link should use some special component in the future. 51}; 52 53function getPlatform(caller) { 54 return caller && caller.platform; 55} 56 57module.exports = ({ types: t, ...api }, { expo }) => { 58 const platform = api.caller(getPlatform); 59 60 function replaceElement(path, state) { 61 // Not supported in node modules 62 if (/\/node_modules\//.test(state.filename)) { 63 return; 64 } 65 66 const { name } = path.node.openingElement.name; 67 68 if (platform === 'web') { 69 if (['html', 'body'].includes(name)) { 70 return; 71 } 72 } 73 // Replace element with @expo/html-elements 74 const component = elementToComponent[name]; 75 76 if (!component) { 77 return; 78 } 79 const prefixedComponent = component; 80 const openingElementName = path.get('openingElement.name'); 81 openingElementName.replaceWith(t.jsxIdentifier(prefixedComponent)); 82 if (path.has('closingElement')) { 83 const closingElementName = path.get('closingElement.name'); 84 closingElementName.replaceWith(t.jsxIdentifier(prefixedComponent)); 85 } 86 state.replacedComponents.add(prefixedComponent); 87 } 88 89 const htmlElementVisitor = { 90 JSXElement(path, state) { 91 replaceElement(path, state); 92 path.traverse(jsxElementVisitor, state); 93 }, 94 }; 95 96 const jsxElementVisitor = { 97 JSXElement(path, state) { 98 replaceElement(path, state); 99 }, 100 }; 101 102 const importDeclarationVisitor = { 103 ImportDeclaration(path, state) { 104 if (path.get('source').isStringLiteral({ value: '@expo/html-elements' })) { 105 state.replacedComponents.forEach((component) => { 106 if ( 107 path 108 .get('specifiers') 109 .some((specifier) => specifier.get('local').isIdentifier({ name: component })) 110 ) { 111 return; 112 } 113 path.pushContainer( 114 'specifiers', 115 t.importSpecifier(t.identifier(component), t.identifier(component)) 116 ); 117 }); 118 } 119 }, 120 }; 121 122 const source = '@expo/html-elements'; 123 return { 124 name: 'Rewrite React DOM to universal Expo elements', 125 visitor: { 126 Program(path, state) { 127 state.replacedComponents = new Set(); 128 state.unsupportedComponents = new Set(); 129 130 path.traverse(htmlElementVisitor, state); 131 132 // If state.replacedComponents is not empty, then ensure `import { ... } from '@expo/html-elements'` is present 133 if (state.replacedComponents.size > 0) { 134 const importDeclaration = t.importDeclaration([], t.stringLiteral(source)); 135 path.unshiftContainer('body', importDeclaration); 136 } 137 138 path.traverse(importDeclarationVisitor, state); 139 }, 140 }, 141 }; 142}; 143