Skip to content

Commit f571399

Browse files
committed
allow jsx elements as jsx attribute values
1 parent a652e73 commit f571399

4 files changed

Lines changed: 42 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
## Unreleased
44

5+
* Allow JSX elements as JSX attribute values
6+
7+
JSX has an obscure feature where you can use JSX elements in attribute position without surrounding them with `{...}`. It looks like this:
8+
9+
```jsx
10+
let el = <div data-ab=<><a/><b/></>/>;
11+
```
12+
13+
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.
14+
15+
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.
16+
517
* Fix a bug with TypeScript type parsing ([#3574](https://github.com/evanw/esbuild/issues/3574))
618

719
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:

internal/js_parser/js_parser.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5045,6 +5045,17 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
50455045
}
50465046
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EString{Value: p.lexer.StringLiteral()}}
50475047
p.lexer.NextInsideJSXElement()
5048+
} else if p.lexer.Token == js_lexer.TLessThan {
5049+
// This may be removed in the future: https://github.com/facebook/jsx/issues/53
5050+
loc := p.lexer.Loc()
5051+
p.lexer.NextInsideJSXElement()
5052+
value = p.parseJSXElement(loc)
5053+
5054+
// The call to parseJSXElement() above doesn't consume the last
5055+
// TGreaterThan because the caller knows what Next() function to call.
5056+
// Use NextJSXElementChild() here since the next token is inside a JSX
5057+
// element.
5058+
p.lexer.NextInsideJSXElement()
50485059
} else {
50495060
// Use Expect() not ExpectInsideJSXElement() so we can parse expression tokens
50505061
p.lexer.Expect(js_lexer.TOpenBrace)

internal/js_parser/js_parser_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5581,6 +5581,21 @@ func TestJSX(t *testing.T) {
55815581
expectParseErrorJSX(t, "<x"+colon+"y"+colon+"/>", "<stdin>: ERROR: Expected \">\" but found \":\"\n")
55825582
expectParseErrorJSX(t, "<x"+colon+"0y/>", "<stdin>: ERROR: Expected identifier after \"x:\" in namespaced JSX name\n")
55835583
}
5584+
5585+
// JSX elements as JSX attribute values
5586+
expectPrintedJSX(t, "<a b=<c/>/>", "/* @__PURE__ */ React.createElement(\"a\", { b: /* @__PURE__ */ React.createElement(\"c\", null) });\n")
5587+
expectPrintedJSX(t, "<a b=<></>/>", "/* @__PURE__ */ React.createElement(\"a\", { b: /* @__PURE__ */ React.createElement(React.Fragment, null) });\n")
5588+
expectParseErrorJSX(t, "<a b=</a>/>", "<stdin>: ERROR: Expected identifier but found \"/\"\n")
5589+
expectParseErrorJSX(t, "<a b=<>/>",
5590+
"<stdin>: WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
5591+
"<stdin>: ERROR: Unexpected end of file before a closing fragment tag\n<stdin>: NOTE: The opening fragment tag is here:\n")
5592+
expectParseErrorJSX(t, "<a b=<c>></a>",
5593+
"<stdin>: WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
5594+
"<stdin>: ERROR: Unexpected closing \"a\" tag does not match opening \"c\" tag\n<stdin>: NOTE: The opening \"c\" tag is here:\n"+
5595+
"<stdin>: ERROR: Expected \">\" but found end of file\n")
5596+
expectParseErrorJSX(t, "<a b=<c>/>",
5597+
"<stdin>: WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
5598+
"<stdin>: ERROR: Unexpected end of file before a closing \"c\" tag\n<stdin>: NOTE: The opening \"c\" tag is here:\n")
55845599
}
55855600

55865601
func TestJSXSingleLine(t *testing.T) {

internal/js_printer/js_printer_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,10 @@ func TestJSX(t *testing.T) {
972972
expectPrintedJSX(t, "<></>", "<></>;\n")
973973
expectPrintedJSX(t, "<>x<y/>z</>", "<>\n {\"x\"}\n <y />\n {\"z\"}\n</>;\n")
974974

975+
expectPrintedJSX(t, "<a b=<c/>/>", "<a b={<c />} />;\n")
976+
expectPrintedJSX(t, "<a b=<>c</>/>", "<a b={<>c</>} />;\n")
977+
expectPrintedJSX(t, "<a b=<>{c}</>/>", "<a b={<>{c}</>} />;\n")
978+
975979
// These can't be escaped because JSX lacks a syntax for escapes
976980
expectPrintedJSXASCII(t, "<π/>", "<π />;\n")
977981
expectPrintedJSXASCII(t, "<π.𐀀/>", "<π.𐀀 />;\n")

0 commit comments

Comments
 (0)