Is it possible for babel to parse a JSX comment?

Viewed 517

The problem

I'm writing a babel plugin that modifies some JSX code at compile time.

<MyComponent >
   {/* comment here */}
</MyComponent>

What I want it to do is read some local configuration and inject some code. I'm aiming for this:

import Something from '/Absolute/path/to/local/config';

// component here:

<MyComponent >
   <SomeHighlyConfigDependentComponent {...withConfiguredProps} />
</MyComponent>

Everything else is fine, but I can't actually get the JSX comment. On AST explorer the AST shows up as:

{
  "type": "JSXExpressionContainer",
  "start": 82,
  "end": 102,
  "expression": {
    "type": "JSXEmptyExpression",
    "start": 83,
    "end": 101
  }
},

But this doesn't give me much information regarding the fact that:

  • this is a comment
  • this comment's contents are equal to comment here

And therefore I can't reliably differentiate this comment ("please inject") from anything else.

Please don't question why I'm doing this; the pseudo code above is a very very minimal, out-of-context concept that is part of a bigger thing in my project. If you have better design alternatives or ideas feel free to comment though!

The question

Is there any way Babel could "detect" the presence of, and parse the contents of, a JSX comment? If not, can you explain why? If not, are there any lightweight alternatives I can try that allows me to do this in compile time (except brute force parsing regex, that's an absolute NO)?

Alternatives I've tried

According to this issue, I've tried this:

<MyComponent >
  { null /* comment here */}
</MyComponent>

And AST explorer parses this as

{
  "type": "JSXExpressionContainer",
  "start": 82,
  "end": 107,
  "expression": {
    "type": "Literal",
    "start": 83,
    "end": 87,
    "value": null,
    "raw": "null"
  }
},

And doesn't solve the problem. Failing to parse comments, I've tried this:

<MyComponent >
  <div data-inject-code="SOME_CONSTANT" />
</MyComponent >

And this works, it's not extremely ideal since we'll have to document how this div does NOT appear in the DOM tree, it gets replaced, and people who actually want a wrapper div around the modified code might be confused about this behavior. Imagine coding <div data-inject-code="SOME_CONSTANT" className="flex flex-row" /> and finding their flexbox wrapper has disappeared.

If all else fails I'll use this solution. But - can you understand what I'm saying here - I'm actually more interested in the "whys" behind the problem. How does babel processes JSX comments, can I make it parse them?

1 Answers

Yes, it is possible. The problem was I was viewing the AST with acorn, which is the default parser, in AST explorer. According to this issue "@babel/parser was born as a fork of Acorn, but it has been completely rewritten." One major difference is that they have different AST formats. Switch the parser to @babel/parser and we'll see a correct representation of how babel parses the AST tree. Also, according to this issue acorn doesn't include comments in its AST, but @babel/parser does.

Comments are not included in the syntax tree that Acorn returns. You can use the onComment option to get information about comments in the file, but Acorn won't put them in the tree for you (as there is no unambiguous way to do so)

    <MyComponent >
    {/* comment */}
    </MyComponent>

This is parsed as a JSXExpressionContainer with an expression value of:

"expression": {
  "type": "JSXEmptyExpression",
  "start": 84,
  "end": 97,
  "loc": {
    "start": {
      "line": 6,
      "column": 5
    },
    "end": {
      "line": 6,
      "column": 18
    }
  },
  "innerComments": [
    {
      "type": "CommentBlock",
      "value": " comment ",
      "start": 84,
      "end": 97,
      "loc": {
        "start": {
          "line": 6,
          "column": 5
        },
        "end": {
          "line": 6,
          "column": 18
        }
      }
    }
  ]
}

Comments can be marked as inner, leading or trailing depending on where they appear in the expression container. This will parse as a trailing comment:

{null /* comment */}

And in this expression, the comment is parsed as a leading comment of the second numeric literal.

{1 + /* comment */ 2}

So we can parse JSX comments (specifically, comments like {/* comment */}) like this:

JSXExpressionContainer: {
  enter(path) {
    const hasComment = path.node.expression.find(expression => expression.type === 'CommentBlock' && expression.value.trim() === 'some value');
  },
}

Here's a very detailed explanation of how babel parses comments: https://github.com/babel/babel/blob/main/packages/babel-parser/ast/comment-attachment.md

  • We construct the comment whitespace in Tokenizer#skipSpace of packages/babel-parser/src/tokenizer/index.js, after we exit from the skip loop, we collect the comments, mark the location info and push to parser.state.commentStack.
  • For every finished AST node invoked from parser#finishNode. Then we iterate state.commentStack reversely. We mark trailingNode when comment.end = node.start, mark containingNode when it is not defined, so here the first finishNode() is the winner, which is exactly the innermost containing node we can attach to comments to.
  • After we set the containing node, we can assign comments to related node. The parser then attaches the comments and do the trailing comma adjustments.
Related