souporserious

Use Babel to Statically Analyze JSX

4 min read

While Babel is popular for transpiling code from new to old syntax, we can also use it to gain static analysis of our code. Setting out to build an editor for JSXUI, I’ve started by looking into how I can use Babel to create a Layers Panel. By the end of this post, we will have a simple plugin we can use to grab all of the JSX elements out of a string of code.

Building the plugin

First, in order to visit any part of our JSX we will use a simple Babel plugin that inherits the JSX syntax plugin:

1import jsx from '@babel/plugin-syntax-jsx'
2
3export default () => {
4 return {
5 inherits: jsx,
6 visitor: {},
7 }
8}

Next, we set up some simple state and store the element’s name and position as we visit each JSX element:

1import jsx from '@babel/plugin-syntax-jsx'
2
3export default () => {
4 return {
5 inherits: jsx,
6 visitor: {
7 Program() {
8 this.tree = []
9 },
10 JSXElement(path) {
11 this.tree.push({
12 name: path.node.openingElement.name.name,
13 start: path.node.start,
14 end: path.node.end,
15 })
16 },
17 },
18 }
19}

We could stop here, but this isn’t enough information about the relationship between our JSX and how it is nested. Scratching my head on how to solve nesting, I took inspiration from the awesome Blocks project. It turns out the ability to enter and exit a node in Babel provides an easy way for us to determine nesting:

1import jsx from '@babel/plugin-syntax-jsx'
2
3export default () => {
4 return {
5 inherits: jsx,
6 visitor: {
7 Program() {
8 this.tree = []
9 },
10 JSXElement: {
11 enter(path) {
12 this.tree.push({
13 name: path.node.openingElement.name.name,
14 start: path.node.start,
15 end: path.node.end,
16 children: [],
17 })
18 },
19 exit() {
20 if (this.tree.length > 1) {
21 const child = this.tree.pop()
22 const parent = this.tree[this.tree.length - 1]
23 parent.children.push(child)
24 }
25 },
26 },
27 },
28 }
29}

This will give us a nicely nested array that matches our JSX.

With the proper information collected, we now need an option to allow our plugin users to access the internal state we’ve built up. We can do this using the options feature of plugins:

1import jsx from '@babel/plugin-syntax-jsx'
2
3export default () => {
4 return {
5 inherits: jsx,
6 visitor: {
7 Program: {
8 enter() {
9 this.tree = []
10 },
11 exit(_, state) {
12 state.opts.onTreeReady(this.tree[0])
13 },
14 },
15 JSXElement: {
16 enter(path) {
17 this.tree.push({
18 name: path.node.openingElement.name.name,
19 start: path.node.start,
20 end: path.node.end,
21 children: [],
22 })
23 },
24 exit() {
25 if (this.tree.length > 1) {
26 const child = this.tree.pop()
27 const parent = this.tree[this.tree.length - 1]
28 parent.children.push(child)
29 }
30 },
31 },
32 },
33 }
34}

Users of our plugin can now pass an onTreeReady function called with the resolved tree upon exiting the Program, which is the first and last node Babel visits.

Using the plugin

Now that we have a plugin that can analyze all of our JSX elements, how do we use it? Depending on our use case, we may be using this in a build step, but for our purposes today, we’ll use the standalone version of Babel that runs in the browser:

1import { transform } from '@babel/standalone'
2
3import plugin from './plugin'
4
5function parseCode(codeString) {
6 return new Promise((resolve) =>
7 transform(codeString, {
8 plugins: [[plugin, { onTreeReady: resolve }]],
9 })
10 )
11}

This wraps up our Babel parsing into a small function that will return a Promise with the resolved tree. Finally, we can build our Layers Panel we sought out originally 🎉

Conclusion

I hope you can take away a little more knowledge of Babel and how powerful it can be for use beyond transpiling. Something we didn’t cover is that anything more complicated than a single function will cause issues with nesting. We could check the immediate function and be smarter about adding elements, but that is beyond the scope of this post. If you have been hesitant to try Babel, I highly suggest diving in and trying to create your own plugins. However, if you want a simple solution for static analysis of your JSX, I recommend the jsx-info or react-scanner packages that makes this easy and provides more detailed information.

Updated:
  • development
  • babel
  • react
  • jsx
Previous post
Bringing SwiftUI Stacks to the Web
Next post
Reading Context in Pragmas