xref: /expo/packages/html-elements/babel.js (revision 14b29cd0)
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