From 71240d4a02afe216ac7d8838369f8ea666c12cc1 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sat, 23 Jan 2021 13:40:35 -0800 Subject: [PATCH] fix #702: add support for namespaces in jsx --- CHANGELOG.md | 27 ++++++++++++++++ internal/js_lexer/js_lexer.go | 48 +++++++++++++++++++++++++--- internal/js_parser/js_parser.go | 2 +- internal/js_parser/js_parser_test.go | 22 ++++++++++++- 4 files changed, 93 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53caa02dbb1..13ce4c7dacd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,33 @@ I recently discovered an interesting discussion about JavaScript syntax entitled ["Most implementations seem to have missed that `await x ** 2` is not legal"](https://github.com/tc39/ecma262/issues/2197). Indeed esbuild has missed this, but this is not surprising because V8 has missed this as well and I usually test esbuild against V8 to test if esbuild is conformant with the JavaScript standard. Regardless, it sounds like the result of the discussion is that the specification should stay the same and implementations should be fixed. This release fixes this bug in esbuild's parser. The syntax `await x ** 2` is no longer allowed and parentheses are now preserved for the syntax `(await x) ** 2`. +* Allow namespaced names in JSX syntax ([#702](https://github.com/evanw/esbuild/issues/702)) + + XML-style namespaced names with a `:` in the middle are a part of the [JSX specification](http://facebook.github.io/jsx/) but they are explicitly unimplemented by React and TypeScript so esbuild doesn't currently support them. However, there was a user request to support this feature since it's part of the JSX specification and esbuild's JSX support can be used for non-React purposes. So this release now supports namespaced names in JSX expressions: + + ```jsx + let xml = + + + Local Record + + + ``` + + This JSX expression is now transformed by esbuild to the following JavaScript: + + ```js + let xml = React.createElement("rdf:RDF", { + "xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "xmlns:dc": "http://purl.org/dc/elements/1.1/" + }, React.createElement("rdf:Description", { + "rdf:ID": "local-record" + }, React.createElement("dc:title", null, "Local Record"))); + ``` + + Note that if you are trying to namespace your React components, this is _not_ the feature to use. You should be using a `.` instead of a `:` for namespacing your React components since `.` resolves to a JavaScript property access. + ## 0.8.34 * Fix a parser bug about suffix expressions after an arrow function body ([#701](https://github.com/evanw/esbuild/issues/701)) diff --git a/internal/js_lexer/js_lexer.go b/internal/js_lexer/js_lexer.go index 8ce8d0ee514..1a351249f48 100644 --- a/internal/js_lexer/js_lexer.go +++ b/internal/js_lexer/js_lexer.go @@ -238,6 +238,7 @@ type Lexer struct { rescanCloseBraceAsTemplateToken bool forGlobalName bool json json + prevErrorLoc logger.Loc // The log is disabled during speculative scans that may backtrack IsLogDisabled bool @@ -247,8 +248,9 @@ type LexerPanic struct{} func NewLexer(log logger.Log, source logger.Source) Lexer { lexer := Lexer{ - log: log, - source: source, + log: log, + source: source, + prevErrorLoc: logger.Loc{Start: -1}, } lexer.step() lexer.Next() @@ -259,6 +261,7 @@ func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer { lexer := Lexer{ log: log, source: source, + prevErrorLoc: logger.Loc{Start: -1}, forGlobalName: true, } lexer.step() @@ -268,8 +271,9 @@ func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer { func NewLexerJSON(log logger.Log, source logger.Source, allowComments bool) Lexer { lexer := Lexer{ - log: log, - source: source, + log: log, + source: source, + prevErrorLoc: logger.Loc{Start: -1}, json: json{ parse: true, allowComments: allowComments, @@ -917,6 +921,24 @@ func (lexer *Lexer) NextInsideJSXElement() { for IsIdentifierContinue(lexer.codePoint) || lexer.codePoint == '-' { lexer.step() } + + // Parse JSX namespaces. These are not supported by React or TypeScript + // but someone using JSX syntax in more obscure ways may find a use for + // them. A namespaced name is just always turned into a string so you + // can't use this feature to reference JavaScript identifiers. + if lexer.codePoint == ':' { + lexer.step() + if IsIdentifierStart(lexer.codePoint) { + lexer.step() + for IsIdentifierContinue(lexer.codePoint) || lexer.codePoint == '-' { + lexer.step() + } + } else { + lexer.addError(logger.Loc{Start: lexer.Range().End()}, + fmt.Sprintf("Expected identifier after %q in namespaced JSX name", lexer.Raw())) + } + } + lexer.Identifier = lexer.Raw() lexer.Token = TIdentifier break @@ -2330,18 +2352,36 @@ func (lexer *Lexer) step() { } func (lexer *Lexer) addError(loc logger.Loc, text string) { + // Don't report multiple errors in the same spot + if loc == lexer.prevErrorLoc { + return + } + lexer.prevErrorLoc = loc + if !lexer.IsLogDisabled { lexer.log.AddError(&lexer.source, loc, text) } } func (lexer *Lexer) addErrorWithNotes(loc logger.Loc, text string, notes []logger.MsgData) { + // Don't report multiple errors in the same spot + if loc == lexer.prevErrorLoc { + return + } + lexer.prevErrorLoc = loc + if !lexer.IsLogDisabled { lexer.log.AddErrorWithNotes(&lexer.source, loc, text, notes) } } func (lexer *Lexer) addRangeError(r logger.Range, text string) { + // Don't report multiple errors in the same spot + if r.Loc == lexer.prevErrorLoc { + return + } + lexer.prevErrorLoc = r.Loc + if !lexer.IsLogDisabled { lexer.log.AddRangeError(&lexer.source, r, text) } diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index f285ac07b1d..348d756f16b 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -3699,7 +3699,7 @@ func (p *parser) parseJSXTag() (logger.Range, string, *js_ast.Expr) { p.lexer.ExpectInsideJSXElement(js_lexer.TIdentifier) // Certain identifiers are strings - if strings.ContainsRune(name, '-') || (p.lexer.Token != js_lexer.TDot && name[0] >= 'a' && name[0] <= 'z') { + if strings.ContainsAny(name, "-:") || (p.lexer.Token != js_lexer.TDot && name[0] >= 'a' && name[0] <= 'z') { return tagRange, name, &js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: js_lexer.StringToUTF16(name)}} } diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index 109ad65b046..802f87f8eb1 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -3001,6 +3001,8 @@ func TestJSX(t *testing.T) { expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(a.b, null);\n") expectPrintedJSX(t, "<_a/>", "/* @__PURE__ */ React.createElement(_a, null);\n") expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a-b\", null);\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a0\", null);\n") + expectParseErrorJSX(t, "<0a/>", ": error: Expected identifier but found \"0\"\n") expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a\", {\n b: true\n});\n") expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a\", {\n b: \"\\\\\"\n});\n") @@ -3119,7 +3121,6 @@ func TestJSX(t *testing.T) { ": error: Expected closing tag \"c.d\" to match opening tag \"a.b\"\n: note: The opening tag \"a.b\" is here\n") expectParseErrorJSX(t, "", ": error: Expected \">\" but found \".\"\n") expectParseErrorJSX(t, "", ": error: Unexpected \"-\"\n") - expectParseErrorJSX(t, "", ": error: Expected \">\" but found \":\"\n") expectParseErrorJSX(t, "{...children}", ": error: Unexpected \"...\"\n") expectPrintedJSX(t, "< /**/ a/>", "/* @__PURE__ */ React.createElement(\"a\", null);\n") @@ -3157,6 +3158,25 @@ func TestJSX(t *testing.T) { expectParseErrorJSX(t, "", ": error: Unexpected end of file\n") expectParseErrorJSX(t, "", "") expectParseErrorJSX(t, "", "") + + // JSX namespaced names + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a:b\", null);\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a-b:c-d\", null);\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a-:b-\", null);\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"Te:st\", null);\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a:b\": true\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-b:c-d\": true\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-:b-\": true\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"Te:st\": true\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a:b\": 0\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-b:c-d\": 0\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-:b-\": 0\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"x\", {\n \"Te:st\": 0\n});\n") + expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a-b\", {\n \"a-b\": a - b\n});\n") + expectParseErrorJSX(t, "", ": error: Expected identifier after \"x:\" in namespaced JSX name\n") + expectParseErrorJSX(t, "", ": error: Expected \">\" but found \":\"\n") + expectParseErrorJSX(t, "", ": error: Expected \">\" but found \":\"\n") + expectParseErrorJSX(t, "", ": error: Expected identifier after \"x:\" in namespaced JSX name\n") } func TestJSXPragmas(t *testing.T) {