diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed7e180a4fe..985a7250309 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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(<>>)
+ ```
+
+ 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))
diff --git a/internal/lexer/lexer.go b/internal/lexer/lexer.go
index fc563af08f0..879833c229a 100644
--- a/internal/lexer/lexer.go
+++ b/internal/lexer/lexer.go
@@ -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
@@ -247,6 +252,8 @@ type Lexer struct {
codePoint rune
StringLiteral []uint16
Identifier string
+ JSXFactoryPragmaComment Span
+ JSXFragmentPragmaComment Span
Number float64
rescanCloseBraceAsTemplateToken bool
json json
@@ -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] == '!'
@@ -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
+ }
}
}
}
diff --git a/internal/parser/parser.go b/internal/parser/parser.go
index 13b9190c7c4..ddcf75312ab 100644
--- a/internal/parser/parser.go
+++ b/internal/parser/parser.go
@@ -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
@@ -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 {
diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go
index 297e4a70064..4c7ac61eaa6 100644
--- a/internal/parser/parser_test.go
+++ b/internal/parser/parser_test.go
@@ -1708,6 +1708,22 @@ func TestJSX(t *testing.T) {
expectPrintedJSX(t, "", "/* @__PURE__ */ React.createElement(\"a\", {\n \U00020000: 0\n});\n")
}
+func TestJSXPragmas(t *testing.T) {
+ expectPrintedJSX(t, "// @jsx h\n", "/* @__PURE__ */ h(\"a\", null);\n")
+ expectPrintedJSX(t, "/* @jsx h */\n", "/* @__PURE__ */ h(\"a\", null);\n")
+ expectPrintedJSX(t, "\n// @jsx h", "/* @__PURE__ */ h(\"a\", null);\n")
+ expectPrintedJSX(t, "\n/* @jsx h */", "/* @__PURE__ */ h(\"a\", null);\n")
+ expectPrintedJSX(t, "// @jsx a.b.c\n", "/* @__PURE__ */ a.b.c(\"a\", null);\n")
+ expectPrintedJSX(t, "/* @jsx a.b.c */\n", "/* @__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",