xref: /expo/packages/html-elements/babel.js (revision bb5069cd)
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
53module.exports = ({ types: t }, { expo }) => {
54  function replaceElement(path, state) {
55    const { name } = path.node.openingElement.name;
56
57    // Replace element with @expo/html-elements
58    const component = elementToComponent[name];
59
60    if (!component) {
61      return;
62    }
63    const prefixedComponent = component;
64    const openingElementName = path.get('openingElement.name');
65    openingElementName.replaceWith(t.jsxIdentifier(prefixedComponent));
66    if (path.has('closingElement')) {
67      const closingElementName = path.get('closingElement.name');
68      closingElementName.replaceWith(t.jsxIdentifier(prefixedComponent));
69    }
70    state.replacedComponents.add(prefixedComponent);
71  }
72
73  const htmlElementVisitor = {
74    JSXElement(path, state) {
75      replaceElement(path, state);
76      path.traverse(jsxElementVisitor, state);
77    },
78  };
79
80  const jsxElementVisitor = {
81    JSXElement(path, state) {
82      replaceElement(path, state);
83    },
84  };
85
86  const importDeclarationVisitor = {
87    ImportDeclaration(path, state) {
88      if (path.get('source').isStringLiteral({ value: '@expo/html-elements' })) {
89        state.replacedComponents.forEach((component) => {
90          if (
91            path
92              .get('specifiers')
93              .some((specifier) => specifier.get('local').isIdentifier({ name: component }))
94          ) {
95            return;
96          }
97          path.pushContainer(
98            'specifiers',
99            t.importSpecifier(t.identifier(component), t.identifier(component))
100          );
101        });
102      }
103    },
104  };
105
106  const source = '@expo/html-elements';
107  return {
108    name: 'Rewrite React DOM to universal Expo elements',
109    visitor: {
110      Program(path, state) {
111        state.replacedComponents = new Set();
112        state.unsupportedComponents = new Set();
113
114        path.traverse(htmlElementVisitor, state);
115
116        // If state.replacedComponents is not empty, then ensure `import { ... } from '@expo/html-elements'` is present
117        if (state.replacedComponents.size > 0) {
118          const importDeclaration = t.importDeclaration([], t.stringLiteral(source));
119          path.unshiftContainer('body', importDeclaration);
120        }
121
122        path.traverse(importDeclarationVisitor, state);
123      },
124    },
125  };
126};
127