Skip to content

Commit

Permalink
fix #702: add support for namespaces in jsx
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 23, 2021
1 parent 39c4cc1 commit 71240d4
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 6 deletions.
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<rdf:Description rdf:ID="local-record">
<dc:title>Local Record</dc:title>
</rdf:Description>
</rdf:RDF>
```

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))
Expand Down
48 changes: 44 additions & 4 deletions internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}}
}

Expand Down
22 changes: 21 additions & 1 deletion internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3001,6 +3001,8 @@ func TestJSX(t *testing.T) {
expectPrintedJSX(t, "<a.b/>", "/* @__PURE__ */ React.createElement(a.b, null);\n")
expectPrintedJSX(t, "<_a/>", "/* @__PURE__ */ React.createElement(_a, null);\n")
expectPrintedJSX(t, "<a-b/>", "/* @__PURE__ */ React.createElement(\"a-b\", null);\n")
expectPrintedJSX(t, "<a0/>", "/* @__PURE__ */ React.createElement(\"a0\", null);\n")
expectParseErrorJSX(t, "<0a/>", "<stdin>: error: Expected identifier but found \"0\"\n")

expectPrintedJSX(t, "<a b/>", "/* @__PURE__ */ React.createElement(\"a\", {\n b: true\n});\n")
expectPrintedJSX(t, "<a b=\"\\\"/>", "/* @__PURE__ */ React.createElement(\"a\", {\n b: \"\\\\\"\n});\n")
Expand Down Expand Up @@ -3119,7 +3121,6 @@ func TestJSX(t *testing.T) {
"<stdin>: error: Expected closing tag \"c.d\" to match opening tag \"a.b\"\n<stdin>: note: The opening tag \"a.b\" is here\n")
expectParseErrorJSX(t, "<a-b.c>", "<stdin>: error: Expected \">\" but found \".\"\n")
expectParseErrorJSX(t, "<a.b-c>", "<stdin>: error: Unexpected \"-\"\n")
expectParseErrorJSX(t, "<a:b>", "<stdin>: error: Expected \">\" but found \":\"\n")
expectParseErrorJSX(t, "<a>{...children}</a>", "<stdin>: error: Unexpected \"...\"\n")

expectPrintedJSX(t, "< /**/ a/>", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
Expand Down Expand Up @@ -3157,6 +3158,25 @@ func TestJSX(t *testing.T) {
expectParseErrorJSX(t, "<a b/**/>", "<stdin>: error: Unexpected end of file\n")
expectParseErrorJSX(t, "<a b/**/ />", "")
expectParseErrorJSX(t, "<a b// \n />", "")

// JSX namespaced names
expectPrintedJSX(t, "<a:b/>", "/* @__PURE__ */ React.createElement(\"a:b\", null);\n")
expectPrintedJSX(t, "<a-b:c-d/>", "/* @__PURE__ */ React.createElement(\"a-b:c-d\", null);\n")
expectPrintedJSX(t, "<a-:b-/>", "/* @__PURE__ */ React.createElement(\"a-:b-\", null);\n")
expectPrintedJSX(t, "<Te:st/>", "/* @__PURE__ */ React.createElement(\"Te:st\", null);\n")
expectPrintedJSX(t, "<x a:b/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a:b\": true\n});\n")
expectPrintedJSX(t, "<x a-b:c-d/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-b:c-d\": true\n});\n")
expectPrintedJSX(t, "<x a-:b-/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-:b-\": true\n});\n")
expectPrintedJSX(t, "<x Te:st/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"Te:st\": true\n});\n")
expectPrintedJSX(t, "<x a:b={0}/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a:b\": 0\n});\n")
expectPrintedJSX(t, "<x a-b:c-d={0}/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-b:c-d\": 0\n});\n")
expectPrintedJSX(t, "<x a-:b-={0}/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"a-:b-\": 0\n});\n")
expectPrintedJSX(t, "<x Te:st={0}/>", "/* @__PURE__ */ React.createElement(\"x\", {\n \"Te:st\": 0\n});\n")
expectPrintedJSX(t, "<a-b a-b={a-b}/>", "/* @__PURE__ */ React.createElement(\"a-b\", {\n \"a-b\": a - b\n});\n")
expectParseErrorJSX(t, "<x:/>", "<stdin>: error: Expected identifier after \"x:\" in namespaced JSX name\n")
expectParseErrorJSX(t, "<x :y/>", "<stdin>: error: Expected \">\" but found \":\"\n")
expectParseErrorJSX(t, "<x:y:/>", "<stdin>: error: Expected \">\" but found \":\"\n")
expectParseErrorJSX(t, "<x:0y/>", "<stdin>: error: Expected identifier after \"x:\" in namespaced JSX name\n")
}

func TestJSXPragmas(t *testing.T) {
Expand Down

0 comments on commit 71240d4

Please sign in to comment.