From f5713992227188d137c485d27b6956c6de814b9a Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Mon, 22 Jan 2024 23:35:12 -0500 Subject: [PATCH] allow jsx elements as jsx attribute values --- CHANGELOG.md | 12 ++++++++++++ internal/js_parser/js_parser.go | 11 +++++++++++ internal/js_parser/js_parser_test.go | 15 +++++++++++++++ internal/js_printer/js_printer_test.go | 4 ++++ 4 files changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea51850c13..866dfafcdf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 =
/>; + ``` + + 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: diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 2da37e8e693..23278096578 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -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) diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index 6487fc55fc5..88eefb54e3f 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -5581,6 +5581,21 @@ func TestJSX(t *testing.T) { expectParseErrorJSX(t, "", ": ERROR: Expected \">\" but found \":\"\n") expectParseErrorJSX(t, "", ": ERROR: Expected identifier after \"x:\" in namespaced JSX name\n") } + + // JSX elements as JSX attribute values + expectPrintedJSX(t, "/>", "/* @__PURE__ */ React.createElement(\"a\", { b: /* @__PURE__ */ React.createElement(\"c\", null) });\n") + expectPrintedJSX(t, "/>", "/* @__PURE__ */ React.createElement(\"a\", { b: /* @__PURE__ */ React.createElement(React.Fragment, null) });\n") + expectParseErrorJSX(t, "/>", ": ERROR: Expected identifier but found \"/\"\n") + expectParseErrorJSX(t, "/>", + ": WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+ + ": ERROR: Unexpected end of file before a closing fragment tag\n: NOTE: The opening fragment tag is here:\n") + expectParseErrorJSX(t, ">", + ": WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+ + ": ERROR: Unexpected closing \"a\" tag does not match opening \"c\" tag\n: NOTE: The opening \"c\" tag is here:\n"+ + ": ERROR: Expected \">\" but found end of file\n") + expectParseErrorJSX(t, "/>", + ": WARNING: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+ + ": ERROR: Unexpected end of file before a closing \"c\" tag\n: NOTE: The opening \"c\" tag is here:\n") } func TestJSXSingleLine(t *testing.T) { diff --git a/internal/js_printer/js_printer_test.go b/internal/js_printer/js_printer_test.go index ecc767fa1f2..a82317658b8 100644 --- a/internal/js_printer/js_printer_test.go +++ b/internal/js_printer/js_printer_test.go @@ -972,6 +972,10 @@ func TestJSX(t *testing.T) { expectPrintedJSX(t, "<>", "<>;\n") expectPrintedJSX(t, "<>xz", "<>\n {\"x\"}\n \n {\"z\"}\n;\n") + expectPrintedJSX(t, "/>", "} />;\n") + expectPrintedJSX(t, "c/>", "c} />;\n") + expectPrintedJSX(t, "{c}/>", "{c}} />;\n") + // These can't be escaped because JSX lacks a syntax for escapes expectPrintedJSXASCII(t, "<π/>", "<π />;\n") expectPrintedJSXASCII(t, "<π.𐀀/>", "<π.𐀀 />;\n")