docz: SyntaxError: Expected corresponding JSX closing tag

Getting SyntaxError: Expected corresponding JSX closing tag error when using any component with children inside the Playground component.

Using components that self close, for example <MyComponent /> works fine.

Here is a sample with the error. Please click on Components > Alert on the left menu.

https://codesandbox.io/s/docz-example-xj522

Thanks

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 11
  • Comments: 15 (6 by maintainers)

Commits related to this issue

Most upvoted comments

I ended up creating a PR with the above fix - https://github.com/doczjs/docz/pull/1696

After a very long debug session (it took a few hours because I was not familiar with the project 😬), I came to the conclusion that:

  • It’s true that this behavior changed because of https://github.com/babel/babel/pull/14105
  • This is a docz bug, which was previously working because it met the exact condition in which the Babel bug that it was accidentally relying on was happening.

What is the fixed Babel bug exactly?

Babel has a few utility methods to control how the AST is traversed. One of them if path.stop(), whose goal is to enterly stop the current traversal. You can read its documentation in our semi-official plugins handbook.

However, in some circumstances it only prevented part of the remaining nodes from being traversed, rather than all of them. We fixed this bug, and now path.stop() behaves properly.


What is the docz bug?

docz has some code to remove the <Playground>...</Playground> tags, transforming

<Playground>
  <Alert>Some text here</Alert>
</Playground>

into

  <Alert>Some text here</Alert>

This is the utility function that should do that transformation: https://github.com/doczjs/docz/blob/259898c25838c052048bd81277d0d74d0ee5c1e2/core/docz-utils/src/jsx.ts#L13-L18

For completeness, here is the codeFromNode function: https://github.com/doczjs/docz/blob/259898c25838c052048bd81277d0d74d0ee5c1e2/core/docz-utils/src/ast.ts#L7-L28

codeFromNode takes a ā€œconditionā€ function, and returns a function that, when given the input code, returns a portion of the code whose AST matches the condition.

const open = codeFromNode(p => p.isJSXOpeningElement()); open(code) returns the the code of something that matches p.isJSXOpeningElement().

In our example above, both <Playground> and <Alert> match that condition: which one does it return? If we look at the valueFromTraverse implementation, it:

  • parses the input source code
  • traverses it with @babel/traverse (Babel uses a DFS traversal, and when using the enter visitor it’s in visits the node in pre-order).
  • when it finds a node that matches the condition, it stores it’s value and calls path.stop() to stop the traversal.

Calling path.stop() as soon as something matches the condition means that it returns the first node that matches it. When looking for a JSXOpeningElement it thus returns <Playground>, while when looking for a JSXClosingElement it’s </Alert>.

(It might help looking at the AST for my example, remembering that Babel traverses nodes in ā€œsource code orderā€: when it comes to JSX, it first traverses the opening tag, then the children and then the closing tag.)

After getting <Playground> and </Alert>, removeTags removes them from the input code.

We were ā€œluckyā€ before because path.stop() didn’t work properly in that case, so it didn’t stop the traversal when it found a closing tag but continued normally (reaching </Playground> at the end of the traversal).


My suggested fix

Rather than finding an opening and a closing tag separately, I suggest getting them at the same time. Also, the current .replace-based approach is quite fragile. For example (even before the @babel/traverse fix), this didn’t work:

<Playground>
  This is followed by a comment! { /* </Playground> */ }
  <Alert>Some text here</Alert>
</Playground>

because by replacing <Playground> and </Playground> with an empty string you get:

  This is followed by a comment! { /*  */ }
  <Alert>Some text here</Alert>
</Playground>

I suggest to rewrite removeTags like this:

const getTagContentsRange = valueFromTraverse(
  p => p.isJSXElement(),
  ({ node }) => {
    if (!node.closingElement) {
      // if the JSX element doesn't have a closingElement, it's because it's self-closed
      // and thus does not have any content: <Playground />
      return [0, 0];
    }
    return [node.openingElement.end, node.closingElement.start];
  }
);

export const removeTags = (code: string) => { 
  const [start, end] = getTagContentsRange(code);
  return code.slice(start, end);
};

by doing so we get the start/end location of the contents of the first JSX tag, which should be safe.

After investigation, it seems that forcing (through yarn resolution or whatever) the downgrade of the package @babel/traverse to v7.16.7 (or @babel/core to v7.16.7) solves the issue. The regression was introduced for versions greater or equal than v7.16.8.

It might be related to this issue https://github.com/babel/babel/issues/14139 and this update https://github.com/babel/babel/pull/14105

Same issue here. For me it seems to be cutting out the first ending tag, so

<Playground>
<div> </div>
<p> </p>
</Playground>

becomes

<Playground>
<div>
<p> </p>
</Playground>

same problem

great work @nicolo-ribaudo, we’re going to merge and release it as soon as we can

Hello @nicolo-ribaudo šŸ‘‹ , First of all, thanks for your time and involvement šŸ™ .

I’ve mentioned the https://github.com/babel/babel/pull/14105 since the issue starts to appear from @babel/traverse@v7.16.8 and above (same issue also with @babel/traverse@7.16.10). From the changelog, I can see that the only change introduced since was https://github.com/babel/babel/pull/14105.

Does docz use any custom Babel plugin to transform <Playground> tags?

I’m not an active Docz maintainer but from what I see, It seems:

Capture d’écran 2022-01-31 aĢ€ 09 11 38

Maybe the maintainers @pedronauck / @renatobenks can confirm.

Hi! Babel maintainer here.

I’d be happy to help figure out if this is a Babel bug, or if it’s a bug somewhere else that was hidden in this specific circumstance and unveiled by that Babel update.

Does docz use any custom Babel plugin to transform <Playground> tags? I only found https://github.com/doczjs/docz/tree/main/other-packages/babel-plugin-export-metadata but it looks unrelated.