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",