Skip to content

Commit

Permalink
fix #138: add support for @jsx and @jsxFrag
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 12, 2020
1 parent dfc2721 commit 5c6b3ab
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 3 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## Unreleased

* Add support for `@jsx` and `@jsxFrag` comments ([#138](https://github.com/evanw/esbuild/issues/138))

You can now override the JSX factory and fragment values on a per-file basis using comments:

```jsx
// @jsx h
// @jsxFrag Fragment
import {h, Fragment} from 'preact'
console.log(<><a/></>)
```

This now generates the following code:

```js
import {h, Fragment} from "preact";
console.log(h(Fragment, null, h("a", null)));
```

## 0.6.2

* Fix code splitting bug with re-export cycles ([#251](https://github.com/evanw/esbuild/issues/251))
Expand Down
77 changes: 74 additions & 3 deletions internal/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ type Comment struct {
Text string
}

type Span struct {
Text string
Range ast.Range
}

type Lexer struct {
log logging.Log
source logging.Source
Expand All @@ -247,6 +252,8 @@ type Lexer struct {
codePoint rune
StringLiteral []uint16
Identifier string
JSXFactoryPragmaComment Span
JSXFragmentPragmaComment Span
Number float64
rescanCloseBraceAsTemplateToken bool
json json
Expand Down Expand Up @@ -2218,6 +2225,62 @@ func (lexer *Lexer) addRangeError(r ast.Range, text string) {
}
}

func hasPrefixWithWordBoundary(text string, prefix string) bool {
t := len(text)
p := len(prefix)
if t >= p && text[0:p] == prefix {
if t == p {
return true
}
c, _ := utf8.DecodeRuneInString(text[p:])
if !IsIdentifierContinue(c) {
return true
}
}
return false
}

func scanForPragmaArg(start int, text string) (Span, bool) {
if text == "" {
return Span{}, false
}

// One or more whitespace characters
c, width := utf8.DecodeRuneInString(text)
if !IsWhitespace(c) {
return Span{}, false
}
for IsWhitespace(c) {
text = text[width:]
start += width
if text == "" {
return Span{}, false
}
c, width = utf8.DecodeRuneInString(text)
}

// One or more non-whitespace characters
i := 0
for !IsWhitespace(c) {
i += width
if i >= len(text) {
break
}
c, width = utf8.DecodeRuneInString(text[i:])
if IsWhitespace(c) {
break
}
}

return Span{
Text: text[:i],
Range: ast.Range{
Loc: ast.Loc{Start: int32(start)},
Len: int32(i),
},
}, true
}

func (lexer *Lexer) scanCommentText() {
text := lexer.source.Contents[lexer.start:lexer.end]
hasPreserveAnnotation := len(text) > 2 && text[2] == '!'
Expand All @@ -2226,16 +2289,24 @@ func (lexer *Lexer) scanCommentText() {
switch text[i] {
case '#':
rest := text[i+1:]
if strings.HasPrefix(rest, "__PURE__") {
if hasPrefixWithWordBoundary(rest, "__PURE__") {
lexer.HasPureCommentBefore = true
}

case '@':
rest := text[i+1:]
if strings.HasPrefix(rest, "__PURE__") {
if hasPrefixWithWordBoundary(rest, "__PURE__") {
lexer.HasPureCommentBefore = true
} else if strings.HasPrefix(rest, "preserve") || strings.HasPrefix(rest, "license") {
} else if hasPrefixWithWordBoundary(rest, "preserve") || hasPrefixWithWordBoundary(rest, "license") {
hasPreserveAnnotation = true
} else if hasPrefixWithWordBoundary(rest, "jsx") {
if arg, ok := scanForPragmaArg(lexer.start+i+1+len("jsx"), rest[len("jsx"):]); ok {
lexer.JSXFactoryPragmaComment = arg
}
} else if hasPrefixWithWordBoundary(rest, "jsxFrag") {
if arg, ok := scanForPragmaArg(lexer.start+i+1+len("jsxFrag"), rest[len("jsxFrag"):]); ok {
lexer.JSXFragmentPragmaComment = arg
}
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -8602,6 +8602,20 @@ func LazyExportAST(log logging.Log, source logging.Source, options config.Option
return ast
}

func (p *parser) validateJSX(span lexer.Span, name string) []string {
if span.Text == "" {
return nil
}
parts := strings.Split(span.Text, ".")
for _, part := range parts {
if !lexer.IsIdentifier(part) {
p.log.AddRangeWarning(&p.source, span.Range, fmt.Sprintf("Invalid JSX %s: %s", name, span.Text))
return nil
}
}
return parts
}

func (p *parser) prepareForVisitPass(options *config.Options) {
p.pushScopeForVisitPass(ast.ScopeEntry, ast.Loc{Start: locModuleScope})
p.moduleScope = p.currentScope
Expand All @@ -8623,6 +8637,16 @@ func (p *parser) prepareForVisitPass(options *config.Options) {
} else {
p.importMetaRef = ast.InvalidRef
}

// Handle "@jsx" and "@jsxFrag" pragmas now that lexing is done
if p.JSX.Parse {
if value := p.validateJSX(p.lexer.JSXFactoryPragmaComment, "factory"); value != nil {
p.JSX.Factory = value
}
if value := p.validateJSX(p.lexer.JSXFragmentPragmaComment, "fragment"); value != nil {
p.JSX.Fragment = value
}
}
}

func (p *parser) declareCommonJSSymbol(kind ast.SymbolKind, name string) ast.Ref {
Expand Down
16 changes: 16 additions & 0 deletions internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,22 @@ func TestJSX(t *testing.T) {
expectPrintedJSX(t, "<a \U00020000={0}/>", "/* @__PURE__ */ React.createElement(\"a\", {\n \U00020000: 0\n});\n")
}

func TestJSXPragmas(t *testing.T) {
expectPrintedJSX(t, "// @jsx h\n<a/>", "/* @__PURE__ */ h(\"a\", null);\n")
expectPrintedJSX(t, "/* @jsx h */\n<a/>", "/* @__PURE__ */ h(\"a\", null);\n")
expectPrintedJSX(t, "<a/>\n// @jsx h", "/* @__PURE__ */ h(\"a\", null);\n")
expectPrintedJSX(t, "<a/>\n/* @jsx h */", "/* @__PURE__ */ h(\"a\", null);\n")
expectPrintedJSX(t, "// @jsx a.b.c\n<a/>", "/* @__PURE__ */ a.b.c(\"a\", null);\n")
expectPrintedJSX(t, "/* @jsx a.b.c */\n<a/>", "/* @__PURE__ */ a.b.c(\"a\", null);\n")

expectPrintedJSX(t, "// @jsxFrag f\n<></>", "/* @__PURE__ */ React.createElement(f, null);\n")
expectPrintedJSX(t, "/* @jsxFrag f */\n<></>", "/* @__PURE__ */ React.createElement(f, null);\n")
expectPrintedJSX(t, "<></>\n// @jsxFrag f", "/* @__PURE__ */ React.createElement(f, null);\n")
expectPrintedJSX(t, "<></>\n/* @jsxFrag f */", "/* @__PURE__ */ React.createElement(f, null);\n")
expectPrintedJSX(t, "// @jsxFrag a.b.c\n<></>", "/* @__PURE__ */ React.createElement(a.b.c, null);\n")
expectPrintedJSX(t, "/* @jsxFrag a.b.c */\n<></>", "/* @__PURE__ */ React.createElement(a.b.c, null);\n")
}

func TestLowerFunctionArgumentScope(t *testing.T) {
templates := []string{
"(x = %s) => {\n};\n",
Expand Down

0 comments on commit 5c6b3ab

Please sign in to comment.