Skip to content

Commit

Permalink
allow jsx elements as jsx attribute values
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 23, 2024
1 parent a652e73 commit f571399
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 0 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## Unreleased

* Allow JSX elements as JSX attribute values

JSX has an obscure feature where you can use JSX elements in attribute position without surrounding them with `{...}`. It looks like this:

```jsx
let el = <div data-ab=<><a/><b/></>/>;
```

I think I originally didn't implement it even though it's part of the [JSX specification](https://facebook.github.io/jsx/) because it previously didn't work in TypeScript (and potentially also in Babel?). However, support for it was [silently added in TypeScript 4.8](https://github.com/microsoft/TypeScript/pull/47994) without me noticing and Babel has also since fixed their [bugs regarding this feature](https://github.com/babel/babel/pull/6006). So I'm adding it to esbuild too now that I know it's widely supported.

Keep in mind that there is some ongoing discussion about [removing this feature from JSX](https://github.com/facebook/jsx/issues/53). I agree that the syntax seems out of place (it does away with the elegance of "JSX is basically just XML with `{...}` escapes" for something arguably harder to read, which doesn't seem like a good trade-off), but it's in the specification and TypeScript and Babel both implement it so I'm going to have esbuild implement it too. However, I reserve the right to remove it from esbuild if it's ever removed from the specification in the future. So use it with caution.

* Fix a bug with TypeScript type parsing ([#3574](https://github.com/evanw/esbuild/issues/3574))

This release fixes a bug with esbuild's TypeScript parser where a conditional type containing a union type that ends with an infer type that ends with a constraint could fail to parse. This was caused by the "don't parse a conditional type" flag not getting passed through the union type parser. Here's an example of valid TypeScript code that previously failed to parse correctly:
Expand Down
11 changes: 11 additions & 0 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5045,6 +5045,17 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
}
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EString{Value: p.lexer.StringLiteral()}}
p.lexer.NextInsideJSXElement()
} else if p.lexer.Token == js_lexer.TLessThan {
// This may be removed in the future: https://github.com/facebook/jsx/issues/53
loc := p.lexer.Loc()
p.lexer.NextInsideJSXElement()
value = p.parseJSXElement(loc)

// The call to parseJSXElement() above doesn't consume the last
// TGreaterThan because the caller knows what Next() function to call.
// Use NextJSXElementChild() here since the next token is inside a JSX
// element.
p.lexer.NextInsideJSXElement()
} else {
// Use Expect() not ExpectInsideJSXElement() so we can parse expression tokens
p.lexer.Expect(js_lexer.TOpenBrace)
Expand Down
15 changes: 15 additions & 0 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5581,6 +5581,21 @@ func TestJSX(t *testing.T) {
expectParseErrorJSX(t, "<x"+colon+"y"+colon+"/>", "<stdin>: ERROR: Expected \">\" but found \":\"\n")
expectParseErrorJSX(t, "<x"+colon+"0y/>", "<stdin>: ERROR: Expected identifier after \"x:\" in namespaced JSX name\n")
}

// JSX elements as JSX attribute values
expectPrintedJSX(t, "<a b=<c/>/>", "/* @__PURE__ */ React.createElement(\"a\", { b: /* @__PURE__ */ React.createElement(\"c\", null) });\n")
expectPrintedJSX(t, "<a b=<></>/>", "/* @__PURE__ */ React.createElement(\"a\", { b: /* @__PURE__ */ React.createElement(React.Fragment, null) });\n")
expectParseErrorJSX(t, "<a b=</a>/>", "<stdin>: ERROR: Expected identifier but found \"/\"\n")
expectParseErrorJSX(t, "<a b=<>/>",
"<stdin>: WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
"<stdin>: ERROR: Unexpected end of file before a closing fragment tag\n<stdin>: NOTE: The opening fragment tag is here:\n")
expectParseErrorJSX(t, "<a b=<c>></a>",
"<stdin>: WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
"<stdin>: ERROR: Unexpected closing \"a\" tag does not match opening \"c\" tag\n<stdin>: NOTE: The opening \"c\" tag is here:\n"+
"<stdin>: ERROR: Expected \">\" but found end of file\n")
expectParseErrorJSX(t, "<a b=<c>/>",
"<stdin>: WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
"<stdin>: ERROR: Unexpected end of file before a closing \"c\" tag\n<stdin>: NOTE: The opening \"c\" tag is here:\n")
}

func TestJSXSingleLine(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions internal/js_printer/js_printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,10 @@ func TestJSX(t *testing.T) {
expectPrintedJSX(t, "<></>", "<></>;\n")
expectPrintedJSX(t, "<>x<y/>z</>", "<>\n {\"x\"}\n <y />\n {\"z\"}\n</>;\n")

expectPrintedJSX(t, "<a b=<c/>/>", "<a b={<c />} />;\n")
expectPrintedJSX(t, "<a b=<>c</>/>", "<a b={<>c</>} />;\n")
expectPrintedJSX(t, "<a b=<>{c}</>/>", "<a b={<>{c}</>} />;\n")

// These can't be escaped because JSX lacks a syntax for escapes
expectPrintedJSXASCII(t, "<π/>", "<π />;\n")
expectPrintedJSXASCII(t, "<π.𐀀/>", "<π.𐀀 />;\n")
Expand Down

0 comments on commit f571399

Please sign in to comment.