>)
+ `,
+ "/custom-react.js": `
+ module.exports = {}
+ `,
+ },
+ entryPaths: []string{"/entry.jsx"},
+ options: config.Options{
+ Mode: config.ModeBundle,
+ JSX: config.JSXOptions{
+ AutomaticRuntime: true,
+ },
+ ExternalSettings: config.ExternalSettings{
+ PreResolve: config.ExternalMatchers{Exact: map[string]bool{
+ "react/jsx-runtime": true,
+ }},
+ },
+ AbsOutputFile: "/out.js",
+ },
+ })
+}
+
+func TestJSXAutomaticImportsES6(t *testing.T) {
+ default_suite.expectBundled(t, bundled{
+ files: map[string]string{
+ "/entry.jsx": `
+ import {jsx, Fragment} from './custom-react'
+ console.log(
, <>
>)
+ `,
+ "/custom-react.js": `
+ export function jsx() {}
+ export function Fragment() {}
+ `,
+ },
+ entryPaths: []string{"/entry.jsx"},
+ options: config.Options{
+ Mode: config.ModeBundle,
+ JSX: config.JSXOptions{
+ AutomaticRuntime: true,
+ },
+ ExternalSettings: config.ExternalSettings{
+ PreResolve: config.ExternalMatchers{Exact: map[string]bool{
+ "react/jsx-runtime": true,
+ }},
+ },
+ AbsOutputFile: "/out.js",
+ },
+ })
+}
+
+func TestJSXAutomaticSyntaxInJS(t *testing.T) {
+ default_suite.expectBundled(t, bundled{
+ files: map[string]string{
+ "/entry.js": `
+ console.log(
)
+ `,
+ },
+ entryPaths: []string{"/entry.js"},
+ options: config.Options{
+ Mode: config.ModeBundle,
+ JSX: config.JSXOptions{
+ AutomaticRuntime: true,
+ },
+ ExternalSettings: config.ExternalSettings{
+ PreResolve: config.ExternalMatchers{Exact: map[string]bool{
+ "react/jsx-runtime": true,
+ }},
+ },
+ AbsOutputFile: "/out.js",
+ },
+ expectedScanLog: `entry.js: ERROR: The JSX syntax extension is not currently enabled
+NOTE: The esbuild loader for this file is currently set to "js" but it must be set to "jsx" to be able to parse JSX syntax. ` +
+ `You can use 'Loader: map[string]api.Loader{".js": api.LoaderJSX}' to do that.
+`,
+ })
+}
+
func TestNodeModules(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
diff --git a/internal/bundler/bundler_tsconfig_test.go b/internal/bundler/bundler_tsconfig_test.go
index 8aa8c74a87a..5fd33baa7aa 100644
--- a/internal/bundler/bundler_tsconfig_test.go
+++ b/internal/bundler/bundler_tsconfig_test.go
@@ -601,6 +601,91 @@ func TestTsConfigNestedJSX(t *testing.T) {
})
}
+func TestTsConfigReactJSX(t *testing.T) {
+ tsconfig_suite.expectBundled(t, bundled{
+ files: map[string]string{
+ "/Users/user/project/entry.tsx": `
+ console.log(<>
>)
+ `,
+ "/Users/user/project/tsconfig.json": `
+ {
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "notreact"
+ }
+ }
+ `,
+ },
+ entryPaths: []string{"/Users/user/project/entry.tsx"},
+ options: config.Options{
+ Mode: config.ModeBundle,
+ AbsOutputFile: "/Users/user/project/out.js",
+ ExternalSettings: config.ExternalSettings{
+ PreResolve: config.ExternalMatchers{Exact: map[string]bool{
+ "notreact/jsx-runtime": true,
+ }},
+ },
+ },
+ })
+}
+
+func TestTsConfigReactJSXDev(t *testing.T) {
+ tsconfig_suite.expectBundled(t, bundled{
+ files: map[string]string{
+ "/Users/user/project/entry.tsx": `
+ console.log(<>
>)
+ `,
+ "/Users/user/project/tsconfig.json": `
+ {
+ "compilerOptions": {
+ "jsx": "react-jsxdev"
+ }
+ }
+ `,
+ },
+ entryPaths: []string{"/Users/user/project/entry.tsx"},
+ options: config.Options{
+ Mode: config.ModeBundle,
+ AbsOutputFile: "/Users/user/project/out.js",
+ ExternalSettings: config.ExternalSettings{
+ PreResolve: config.ExternalMatchers{Exact: map[string]bool{
+ "react/jsx-dev-runtime": true,
+ }},
+ },
+ },
+ })
+}
+
+func TestTsConfigReactJSXWithDevInMainConfig(t *testing.T) {
+ tsconfig_suite.expectBundled(t, bundled{
+ files: map[string]string{
+ "/Users/user/project/entry.tsx": `
+ console.log(<>
>)
+ `,
+ "/Users/user/project/tsconfig.json": `
+ {
+ "compilerOptions": {
+ "jsx": "react-jsx"
+ }
+ }
+ `,
+ },
+ entryPaths: []string{"/Users/user/project/entry.tsx"},
+ options: config.Options{
+ Mode: config.ModeBundle,
+ AbsOutputFile: "/Users/user/project/out.js",
+ JSX: config.JSXOptions{
+ Development: true,
+ },
+ ExternalSettings: config.ExternalSettings{
+ PreResolve: config.ExternalMatchers{Exact: map[string]bool{
+ "react/jsx-dev-runtime": true,
+ }},
+ },
+ },
+ })
+}
+
func TestTsconfigJsonBaseUrl(t *testing.T) {
tsconfig_suite.expectBundled(t, bundled{
files: map[string]string{
diff --git a/internal/bundler/snapshots/snapshots_default.txt b/internal/bundler/snapshots/snapshots_default.txt
index d135cacd25b..4816fecf9c7 100644
--- a/internal/bundler/snapshots/snapshots_default.txt
+++ b/internal/bundler/snapshots/snapshots_default.txt
@@ -1415,6 +1415,42 @@ console.log(replace.test);
console.log(collide);
console.log(re_export);
+================================================================================
+TestJSXAutomaticImportsCommonJS
+---------- /out.js ----------
+// custom-react.js
+var require_custom_react = __commonJS({
+ "custom-react.js"(exports, module) {
+ module.exports = {};
+ }
+});
+
+// entry.jsx
+var import_custom_react = __toESM(require_custom_react());
+import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
+console.log(/* @__PURE__ */ jsx2("div", {
+ jsx: import_custom_react.jsx
+}), /* @__PURE__ */ jsx2(Fragment2, {
+ children: /* @__PURE__ */ jsx2(import_custom_react.Fragment, {})
+}));
+
+================================================================================
+TestJSXAutomaticImportsES6
+---------- /out.js ----------
+// custom-react.js
+function jsx() {
+}
+function Fragment() {
+}
+
+// entry.jsx
+import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
+console.log(/* @__PURE__ */ jsx2("div", {
+ jsx
+}), /* @__PURE__ */ jsx2(Fragment2, {
+ children: /* @__PURE__ */ jsx2(Fragment, {})
+}));
+
================================================================================
TestJSXConstantFragments
---------- /out.js ----------
diff --git a/internal/bundler/snapshots/snapshots_tsconfig.txt b/internal/bundler/snapshots/snapshots_tsconfig.txt
index 3bf3e3b6f01..63831a5ca01 100644
--- a/internal/bundler/snapshots/snapshots_tsconfig.txt
+++ b/internal/bundler/snapshots/snapshots_tsconfig.txt
@@ -314,6 +314,66 @@ function fib(input) {
// Users/user/project/entry.ts
console.log(fib(10));
+================================================================================
+TestTsConfigReactJSX
+---------- /Users/user/project/out.js ----------
+// Users/user/project/entry.tsx
+import { Fragment, jsx, jsxs } from "notreact/jsx-runtime";
+console.log(/* @__PURE__ */ jsxs(Fragment, {
+ children: [
+ /* @__PURE__ */ jsx("div", {}),
+ /* @__PURE__ */ jsx("div", {})
+ ]
+}));
+
+================================================================================
+TestTsConfigReactJSXDev
+---------- /Users/user/project/out.js ----------
+// Users/user/project/entry.tsx
+import { Fragment, jsxDEV } from "react/jsx-dev-runtime";
+console.log(/* @__PURE__ */ jsxDEV(Fragment, {
+ children: [
+ /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
+ fileName: "Users/user/project/entry.tsx",
+ lineNumber: 2,
+ columnNumber: 19
+ }, this),
+ /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
+ fileName: "Users/user/project/entry.tsx",
+ lineNumber: 2,
+ columnNumber: 25
+ }, this)
+ ]
+}, void 0, true, {
+ fileName: "Users/user/project/entry.tsx",
+ lineNumber: 2,
+ columnNumber: 17
+}, this));
+
+================================================================================
+TestTsConfigReactJSXWithDevInMainConfig
+---------- /Users/user/project/out.js ----------
+// Users/user/project/entry.tsx
+import { Fragment, jsxDEV } from "react/jsx-dev-runtime";
+console.log(/* @__PURE__ */ jsxDEV(Fragment, {
+ children: [
+ /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
+ fileName: "Users/user/project/entry.tsx",
+ lineNumber: 2,
+ columnNumber: 19
+ }, this),
+ /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
+ fileName: "Users/user/project/entry.tsx",
+ lineNumber: 2,
+ columnNumber: 25
+ }, this)
+ ]
+}, void 0, true, {
+ fileName: "Users/user/project/entry.tsx",
+ lineNumber: 2,
+ columnNumber: 17
+}, this));
+
================================================================================
TestTsConfigWithStatementAlwaysStrictFalse
---------- /Users/user/project/out.js ----------
diff --git a/internal/config/config.go b/internal/config/config.go
index 28d7fe5ac38..043313fd970 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -13,10 +13,39 @@ import (
)
type JSXOptions struct {
- Factory DefineExpr
- Fragment DefineExpr
- Parse bool
- Preserve bool
+ Factory DefineExpr
+ Fragment DefineExpr
+ Parse bool
+ Preserve bool
+ AutomaticRuntime bool
+ ImportSource string
+ Development bool
+}
+
+type TSJSX uint8
+
+const (
+ TSJSXNone TSJSX = iota
+ TSJSXPreserve
+ TSJSXReact
+ TSJSXReactJSX
+ TSJSXReactJSXDev
+)
+
+func (jsxOptions *JSXOptions) SetOptionsFromTSJSX(tsx TSJSX) {
+ switch tsx {
+ case TSJSXPreserve:
+ jsxOptions.Preserve = true
+ case TSJSXReact:
+ jsxOptions.AutomaticRuntime = false
+ jsxOptions.Development = false
+ case TSJSXReactJSX:
+ jsxOptions.AutomaticRuntime = true
+ // Don't set Development = false implicitly
+ case TSJSXReactJSXDev:
+ jsxOptions.AutomaticRuntime = true
+ jsxOptions.Development = true
+ }
}
type TSOptions struct {
@@ -276,6 +305,7 @@ type Options struct {
WriteToStdout bool
OmitRuntimeForTests bool
+ OmitJSXRuntimeForTests bool
UnusedImportFlagsTS UnusedImportFlagsTS
UseDefineForClassFields MaybeBool
ASCIIOnly bool
diff --git a/internal/js_ast/js_ast.go b/internal/js_ast/js_ast.go
index f67a93f827f..55fc0c233f5 100644
--- a/internal/js_ast/js_ast.go
+++ b/internal/js_ast/js_ast.go
@@ -1525,6 +1525,7 @@ const (
ImplicitStrictModeClass
ImplicitStrictModeESM
ImplicitStrictModeTSAlwaysStrict
+ ImplicitStrictModeJSXAutomaticRuntime
)
func (s *Scope) RecursiveSetStrictMode(kind StrictModeKind) {
diff --git a/internal/js_lexer/js_lexer.go b/internal/js_lexer/js_lexer.go
index 72b1a5bfbe1..b25189b3b9a 100644
--- a/internal/js_lexer/js_lexer.go
+++ b/internal/js_lexer/js_lexer.go
@@ -245,15 +245,17 @@ type MaybeSubstring struct {
}
type Lexer struct {
- CommentsToPreserveBefore []js_ast.Comment
- AllOriginalComments []js_ast.Comment
- Identifier MaybeSubstring
- log logger.Log
- source logger.Source
- JSXFactoryPragmaComment logger.Span
- JSXFragmentPragmaComment logger.Span
- SourceMappingURL logger.Span
- BadArrowInTSXSuggestion string
+ CommentsToPreserveBefore []js_ast.Comment
+ AllOriginalComments []js_ast.Comment
+ Identifier MaybeSubstring
+ log logger.Log
+ source logger.Source
+ JSXFactoryPragmaComment logger.Span
+ JSXFragmentPragmaComment logger.Span
+ JSXRuntimePragmaComment logger.Span
+ JSXImportSourcePragmaComment logger.Span
+ SourceMappingURL logger.Span
+ BadArrowInTSXSuggestion string
// Escape sequences in string literals are decoded lazily because they are
// not interpreted inside tagged templates, and tagged templates can contain
@@ -2786,6 +2788,14 @@ func (lexer *Lexer) scanCommentText() {
if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsxFrag", rest); ok {
lexer.JSXFragmentPragmaComment = arg
}
+ } else if hasPrefixWithWordBoundary(rest, "jsxRuntime") {
+ if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsxRuntime", rest); ok {
+ lexer.JSXRuntimePragmaComment = arg
+ }
+ } else if hasPrefixWithWordBoundary(rest, "jsxImportSource") {
+ if arg, ok := scanForPragmaArg(pragmaSkipSpaceFirst, lexer.start+i+1, "jsxImportSource", rest); ok {
+ lexer.JSXImportSourcePragmaComment = arg
+ }
} else if i == 2 && strings.HasPrefix(rest, " sourceMappingURL=") {
if arg, ok := scanForPragmaArg(pragmaNoSpaceFirst, lexer.start+i+1, " sourceMappingURL=", rest); ok {
lexer.SourceMappingURL = arg
diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go
index 35d8529688a..90b64074f03 100644
--- a/internal/js_parser/js_parser.go
+++ b/internal/js_parser/js_parser.go
@@ -194,6 +194,11 @@ type parser struct {
tempRefCount int
topLevelTempRefCount int
+ // We need to scan over the source contents to recover the line and column offsets
+ jsxSourceLoc int
+ jsxSourceLine int
+ jsxSourceColumn int
+
exportsRef js_ast.Ref
requireRef js_ast.Ref
moduleRef js_ast.Ref
@@ -204,6 +209,11 @@ type parser struct {
superCtorRef js_ast.Ref
jsxDevRef js_ast.Ref
+ // Imports from "react/jsx-runtime" and "react", respectively.
+ // (Or whatever was specified in the "importSource" option)
+ jsxRuntimeImports map[string]js_ast.Ref
+ jsxLegacyImports map[string]js_ast.Ref
+
// For lowering private methods
weakMapRef js_ast.Ref
weakSetRef js_ast.Ref
@@ -216,6 +226,7 @@ type parser struct {
latestArrowArgLoc logger.Loc
forbidSuffixAfterAsLoc logger.Loc
+ firstJSXElementLoc logger.Loc
fnOrArrowDataVisit fnOrArrowDataVisit
@@ -396,6 +407,7 @@ type optionsThatSupportStructuralEquality struct {
minifySyntax bool
minifyIdentifiers bool
omitRuntimeForTests bool
+ omitJSXRuntimeForTests bool
ignoreDCEAnnotations bool
treeShaking bool
dropDebugger bool
@@ -430,6 +442,7 @@ func OptionsFromConfig(options *config.Options) Options {
minifySyntax: options.MinifySyntax,
minifyIdentifiers: options.MinifyIdentifiers,
omitRuntimeForTests: options.OmitRuntimeForTests,
+ omitJSXRuntimeForTests: options.OmitJSXRuntimeForTests,
ignoreDCEAnnotations: options.IgnoreDCEAnnotations,
treeShaking: options.TreeShaking,
dropDebugger: options.DropDebugger,
@@ -1515,6 +1528,55 @@ func (p *parser) callRuntime(loc logger.Loc, name string, args []js_ast.Expr) js
}}
}
+type JSXImport uint8
+
+const (
+ JSXImportJSX JSXImport = iota
+ JSXImportJSXS
+ JSXImportFragment
+ JSXImportCreateElement
+)
+
+func (p *parser) importJSXSymbol(loc logger.Loc, jsx JSXImport) js_ast.Expr {
+ var symbols map[string]js_ast.Ref
+ var name string
+
+ switch jsx {
+ case JSXImportJSX:
+ symbols = p.jsxRuntimeImports
+ if p.options.jsx.Development {
+ name = "jsxDEV"
+ } else {
+ name = "jsx"
+ }
+ case JSXImportJSXS:
+ symbols = p.jsxRuntimeImports
+ if p.options.jsx.Development {
+ name = "jsxDEV"
+ } else {
+ name = "jsxs"
+ }
+ case JSXImportFragment:
+ symbols = p.jsxRuntimeImports
+ name = "Fragment"
+ case JSXImportCreateElement:
+ symbols = p.jsxLegacyImports
+ name = "createElement"
+ }
+
+ ref, ok := symbols[name]
+ if !ok {
+ ref = p.newSymbol(js_ast.SymbolOther, name)
+ p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
+ p.isImportItem[ref] = true
+ symbols[name] = ref
+ }
+ p.recordUsage(ref)
+ return p.handleIdentifier(loc, &js_ast.EIdentifier{Ref: ref}, identifierOpts{
+ wasOriginallyIdentifier: true,
+ })
+}
+
func (p *parser) valueToSubstituteForRequire(loc logger.Loc) js_ast.Expr {
if p.source.Index != runtime.SourceIndex &&
config.ShouldCallRuntimeRequire(p.options.mode, p.options.outputFormat) {
@@ -4455,6 +4517,11 @@ func (p *parser) parseJSXTag() (logger.Range, string, js_ast.Expr) {
}
func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
+ // Keep track of the location of the first JSX element for error messages
+ if p.firstJSXElementLoc.Start == -1 {
+ p.firstJSXElementLoc = loc
+ }
+
// Parse the tag
startRange, startText, startTagOrNil := p.parseJSXTag()
@@ -12058,6 +12125,38 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
case *js_ast.EJSXElement:
propsLoc := expr.Loc
+
+ // Resolving the location index to a specific line and column in
+ // development mode is not too expensive because we seek from the
+ // previous JSX element. It amounts to at most a single additional
+ // scan over the source code. Note that this has to happen before
+ // we visit anything about this JSX element to make sure that we
+ // only ever need to scan forward, not backward.
+ var jsxSourceLine int
+ var jsxSourceColumn int
+ if p.options.jsx.Development && p.options.jsx.AutomaticRuntime {
+ for p.jsxSourceLoc < int(propsLoc.Start) {
+ r, size := utf8.DecodeRuneInString(p.source.Contents[p.jsxSourceLoc:])
+ p.jsxSourceLoc += size
+ if r == '\n' || r == '\r' || r == '\u2028' || r == '\u2029' {
+ if r == '\r' && p.jsxSourceLoc < len(p.source.Contents) && p.source.Contents[p.jsxSourceLoc] == '\n' {
+ p.jsxSourceLoc++ // Handle Windows-style CRLF newlines
+ }
+ p.jsxSourceLine++
+ p.jsxSourceColumn = 0
+ } else {
+ // Babel and TypeScript count columns in UTF-16 code units
+ if r < 0xFFFF {
+ p.jsxSourceColumn++
+ } else {
+ p.jsxSourceColumn += 2
+ }
+ }
+ }
+ jsxSourceLine = p.jsxSourceLine
+ jsxSourceColumn = p.jsxSourceColumn
+ }
+
if e.TagOrNil.Data != nil {
propsLoc = e.TagOrNil.Loc
e.TagOrNil = p.visitExpr(e.TagOrNil)
@@ -12101,39 +12200,195 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
} else {
// A missing tag is a fragment
if e.TagOrNil.Data == nil {
- e.TagOrNil = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Fragment, identifierOpts{
- wasOriginallyIdentifier: true,
- matchAgainstDefines: true, // Allow defines to rewrite the JSX fragment factory
- })
+ if p.options.jsx.AutomaticRuntime {
+ e.TagOrNil = p.importJSXSymbol(expr.Loc, JSXImportFragment)
+ } else {
+ e.TagOrNil = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Fragment, identifierOpts{
+ wasOriginallyIdentifier: true,
+ matchAgainstDefines: true, // Allow defines to rewrite the JSX fragment factory
+ })
+ }
+ }
+
+ shouldUseCreateElement := !p.options.jsx.AutomaticRuntime
+ if !shouldUseCreateElement {
+ // Even for runtime="automatic",
is special cased to createElement
+ // See https://github.com/babel/babel/blob/e482c763466ba3f44cb9e3467583b78b7f030b4a/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts#L352
+ seenPropsSpread := false
+ for _, property := range e.Properties {
+ if seenPropsSpread && property.Kind == js_ast.PropertyNormal {
+ if str, ok := property.Key.Data.(*js_ast.EString); ok && helpers.UTF16EqualsString(str.Value, "key") {
+ shouldUseCreateElement = true
+ break
+ }
+ } else if property.Kind == js_ast.PropertySpread {
+ seenPropsSpread = true
+ }
+ }
}
- // Arguments to createElement()
- args := []js_ast.Expr{e.TagOrNil}
- if len(e.Properties) > 0 {
+ if shouldUseCreateElement {
+ // Arguments to createElement()
+ args := []js_ast.Expr{e.TagOrNil}
+ if len(e.Properties) > 0 {
+ args = append(args, p.lowerObjectSpread(propsLoc, &js_ast.EObject{
+ Properties: e.Properties,
+ }))
+ } else {
+ args = append(args, js_ast.Expr{Loc: propsLoc, Data: js_ast.ENullShared})
+ }
+ if len(e.Children) > 0 {
+ args = append(args, e.Children...)
+ }
+
+ // Call createElement()
+ var target js_ast.Expr
+ if p.options.jsx.AutomaticRuntime {
+ target = p.importJSXSymbol(expr.Loc, JSXImportCreateElement)
+ } else {
+ target = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Factory, identifierOpts{
+ wasOriginallyIdentifier: true,
+ matchAgainstDefines: true, // Allow defines to rewrite the JSX factory
+ })
+ p.warnAboutImportNamespaceCall(target, exprKindCall)
+ }
+ return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
+ Target: target,
+ Args: args,
+ CloseParenLoc: e.CloseLoc,
+
+ // Enable tree shaking
+ CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations,
+ }}, exprOut{}
+ } else {
+ // Arguments to jsx()
+ args := []js_ast.Expr{e.TagOrNil}
+
+ // Props argument
+ properties := make([]js_ast.Property, 0, len(e.Properties)+1)
+
+ // For jsx(), "key" is passed in as a separate argument, so filter it out
+ // from the props here. Also, check for __source and __self, which might have
+ // been added by some upstream plugin. Their presence here would represent a
+ // configuration error.
+ hasKey := false
+ keyProperty := js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared}
+ for _, property := range e.Properties {
+ if str, ok := property.Key.Data.(*js_ast.EString); ok {
+ propName := helpers.UTF16ToString(str.Value)
+ switch propName {
+ case "key":
+ if property.Flags.Has(js_ast.PropertyWasShorthand) {
+ r := js_lexer.RangeOfIdentifier(p.source, property.Loc)
+ msg := logger.Msg{
+ Kind: logger.Error,
+ Data: p.tracker.MsgData(r, "Please provide an explicit value for \"key\":"),
+ Notes: []logger.MsgData{{Text: "Using \"key\" as a shorthand for \"key={true}\" is not allowed when using React's \"automatic\" JSX transform."}},
+ }
+ msg.Data.Location.Suggestion = "key={true}"
+ p.log.AddMsg(msg)
+ } else {
+ keyProperty = property.ValueOrNil
+ hasKey = true
+ }
+ continue
+
+ case "__source", "__self":
+ r := js_lexer.RangeOfIdentifier(p.source, property.Loc)
+ p.log.AddErrorWithNotes(&p.tracker, r,
+ fmt.Sprintf("Duplicate \"%s\" prop found:", propName),
+ []logger.MsgData{{Text: "Both \"__source\" and \"__self\" are set automatically by esbuild when using React's \"automatic\" JSX transform. " +
+ "This duplicate prop may have come from a plugin."}})
+ continue
+ }
+ }
+ properties = append(properties, property)
+ }
+
+ isStaticChildren := len(e.Children) > 1
+
+ // Children are passed in as an explicit prop
+ if len(e.Children) > 0 {
+ childrenValue := e.Children[0]
+
+ if len(e.Children) > 1 {
+ childrenValue.Data = &js_ast.EArray{Items: e.Children}
+ } else if _, ok := childrenValue.Data.(*js_ast.ESpread); ok {
+ // TypeScript considers spread children to be static, but Babel considers
+ // it to be an error ("Spread children are not supported in React.").
+ // We'll follow TypeScript's behavior here because spread children may be
+ // valid with non-React source runtimes.
+ childrenValue.Data = &js_ast.EArray{Items: []js_ast.Expr{childrenValue}}
+ isStaticChildren = true
+ }
+
+ properties = append(properties, js_ast.Property{
+ Key: js_ast.Expr{
+ Data: &js_ast.EString{Value: helpers.StringToUTF16("children")},
+ Loc: childrenValue.Loc,
+ },
+ ValueOrNil: childrenValue,
+ Kind: js_ast.PropertyNormal,
+ Loc: childrenValue.Loc,
+ })
+ }
+
args = append(args, p.lowerObjectSpread(propsLoc, &js_ast.EObject{
- Properties: e.Properties,
+ Properties: properties,
}))
- } else {
- args = append(args, js_ast.Expr{Loc: propsLoc, Data: js_ast.ENullShared})
- }
- if len(e.Children) > 0 {
- args = append(args, e.Children...)
- }
- // Call createElement()
- target := p.instantiateDefineExpr(expr.Loc, p.options.jsx.Factory, identifierOpts{
- wasOriginallyIdentifier: true,
- matchAgainstDefines: true, // Allow defines to rewrite the JSX factory
- })
- p.warnAboutImportNamespaceCall(target, exprKindCall)
- return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
- Target: target,
- Args: args,
- CloseParenLoc: e.CloseLoc,
+ // "key"
+ if hasKey || p.options.jsx.Development {
+ args = append(args, keyProperty)
+ }
+
+ if p.options.jsx.Development {
+ // "isStaticChildren"
+ args = append(args, js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: isStaticChildren}})
+
+ // "__source"
+ args = append(args, js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EObject{
+ Properties: []js_ast.Property{
+ {
+ Kind: js_ast.PropertyNormal,
+ Key: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("fileName")}},
+ ValueOrNil: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(p.source.PrettyPath)}},
+ },
+ {
+ Kind: js_ast.PropertyNormal,
+ Key: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("lineNumber")}},
+ ValueOrNil: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(jsxSourceLine + 1)}}, // 1-based lines
+ },
+ {
+ Kind: js_ast.PropertyNormal,
+ Key: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16("columnNumber")}},
+ ValueOrNil: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(jsxSourceColumn + 1)}}, // 1-based columns
+ },
+ },
+ }})
+
+ // "__self"
+ if p.fnOrArrowDataParse.isThisDisallowed {
+ args = append(args, js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared})
+ } else {
+ args = append(args, js_ast.Expr{Loc: expr.Loc, Data: js_ast.EThisShared})
+ }
+ }
- // Enable tree shaking
- CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations,
- }}, exprOut{}
+ jsx := JSXImportJSX
+ if isStaticChildren {
+ jsx = JSXImportJSXS
+ }
+
+ return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
+ Target: p.importJSXSymbol(expr.Loc, jsx),
+ Args: args,
+ CloseParenLoc: e.CloseLoc,
+
+ // Enable tree shaking
+ CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations,
+ }}, exprOut{}
+ }
}
case *js_ast.ETemplate:
@@ -15360,6 +15615,7 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio
promiseRef: js_ast.InvalidRef,
regExpRef: js_ast.InvalidRef,
afterArrowBodyLoc: logger.Loc{Start: -1},
+ firstJSXElementLoc: logger.Loc{Start: -1},
importMetaRef: js_ast.InvalidRef,
runtimePublicFieldImport: js_ast.InvalidRef,
superCtorRef: js_ast.InvalidRef,
@@ -15383,6 +15639,10 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio
namedImports: make(map[js_ast.Ref]js_ast.NamedImport),
namedExports: make(map[string]js_ast.NamedExport),
+ // For JSX runtime imports
+ jsxRuntimeImports: make(map[string]js_ast.Ref),
+ jsxLegacyImports: make(map[string]js_ast.Ref),
+
suppressWarningsAboutWeirdCode: helpers.IsInsideNodeModules(source.KeyPath.Text),
}
@@ -15398,6 +15658,8 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio
var defaultJSXFactory = []string{"React", "createElement"}
var defaultJSXFragment = []string{"React", "Fragment"}
+const defaultJSXImportSource = "react"
+
func Parse(log logger.Log, source logger.Source, options Options) (result js_ast.AST, ok bool) {
ok = true
defer func() {
@@ -15416,6 +15678,9 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast
if len(options.jsx.Fragment.Parts) == 0 && options.jsx.Fragment.Constant == nil {
options.jsx.Fragment = config.DefineExpr{Parts: defaultJSXFragment}
}
+ if len(options.jsx.ImportSource) == 0 {
+ options.jsx.ImportSource = defaultJSXImportSource
+ }
if !options.ts.Parse {
// Non-TypeScript files always get the real JavaScript class field behavior
@@ -15531,7 +15796,7 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast
}
}
}
- before = p.generateImportStmt(file.Source.KeyPath.Text, exportsNoConflict, file.Source.Index, before, symbols)
+ before = p.generateImportStmt(file.Source.KeyPath.Text, exportsNoConflict, &file.Source.Index, before, symbols)
}
// Bind symbols in a second pass over the AST. I started off doing this in a
@@ -15606,8 +15871,7 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast
// Pop the module scope to apply the "ContainsDirectEval" rules
p.popScope()
- parts = append(append(before, parts...), after...)
- result = p.toAST(parts, hashbang, directive)
+ result = p.toAST(before, parts, after, hashbang, directive)
result.SourceMapComment = p.lexer.SourceMappingURL
return
}
@@ -15638,18 +15902,11 @@ func LazyExportAST(log logger.Log, source logger.Source, options Options, expr j
}
p.symbolUses = nil
- ast := p.toAST([]js_ast.Part{nsExportPart, part}, "", "")
+ ast := p.toAST(nil, []js_ast.Part{nsExportPart, part}, nil, "", "")
ast.HasLazyExport = true
return ast
}
-type JSXExprKind uint8
-
-const (
- JSXFactory JSXExprKind = iota
- JSXFragment
-)
-
func ParseDefineExprOrJSON(text string) (config.DefineExpr, js_ast.E) {
if text == "" {
return config.DefineExpr{}, nil
@@ -15785,22 +16042,60 @@ func (p *parser) prepareForVisitPass() {
// Handle "@jsx" and "@jsxFrag" pragmas now that lexing is done
if p.options.jsx.Parse {
+ if jsxRuntime := p.lexer.JSXRuntimePragmaComment; jsxRuntime.Text != "" {
+ if jsxRuntime.Text == "automatic" {
+ p.options.jsx.AutomaticRuntime = true
+ } else if jsxRuntime.Text == "classic" {
+ p.options.jsx.AutomaticRuntime = false
+ } else {
+ p.log.AddIDWithNotes(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxRuntime.Range,
+ fmt.Sprintf("Invalid JSX runtime: %q", jsxRuntime.Text),
+ []logger.MsgData{{Text: "The JSX runtime can only be set to either \"classic\" or \"automatic\"."}})
+ }
+ }
+
if jsxFactory := p.lexer.JSXFactoryPragmaComment; jsxFactory.Text != "" {
- if expr, _ := ParseDefineExprOrJSON(jsxFactory.Text); len(expr.Parts) > 0 {
+ if p.options.jsx.AutomaticRuntime {
+ p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFactory.Range,
+ "The JSX factory cannot be set when using React's \"automatic\" JSX transform")
+ } else if expr, _ := ParseDefineExprOrJSON(jsxFactory.Text); len(expr.Parts) > 0 {
p.options.jsx.Factory = expr
} else {
p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFactory.Range,
fmt.Sprintf("Invalid JSX factory: %s", jsxFactory.Text))
}
}
+
if jsxFragment := p.lexer.JSXFragmentPragmaComment; jsxFragment.Text != "" {
- if expr, _ := ParseDefineExprOrJSON(jsxFragment.Text); len(expr.Parts) > 0 || expr.Constant != nil {
+ if p.options.jsx.AutomaticRuntime {
+ p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFragment.Range,
+ "The JSX fragment cannot be set when using React's \"automatic\" JSX transform")
+ } else if expr, _ := ParseDefineExprOrJSON(jsxFragment.Text); len(expr.Parts) > 0 || expr.Constant != nil {
p.options.jsx.Fragment = expr
} else {
p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFragment.Range,
fmt.Sprintf("Invalid JSX fragment: %s", jsxFragment.Text))
}
}
+
+ if jsxImportSource := p.lexer.JSXImportSourcePragmaComment; jsxImportSource.Text != "" {
+ if !p.options.jsx.AutomaticRuntime {
+ p.log.AddIDWithNotes(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxImportSource.Range,
+ fmt.Sprintf("The JSX import source cannot be set without also enabling React's \"automatic\" JSX transform"),
+ []logger.MsgData{{Text: "You can enable React's \"automatic\" JSX transform for this file by using a \"@jsxRuntime automatic\" comment."}})
+ } else {
+ p.options.jsx.ImportSource = jsxImportSource.Text
+ }
+ }
+ }
+
+ // Force-enable strict mode if the JSX "automatic" runtime is enabled and
+ // there is at least one JSX element. This is because the automatically-
+ // generated import statement turns the file into an ES module. This behavior
+ // matches TypeScript which also does this. See this PR for more information:
+ // https://github.com/microsoft/TypeScript/pull/39199
+ if p.currentScope.StrictMode == js_ast.SloppyMode && p.options.jsx.AutomaticRuntime && p.firstJSXElementLoc.Start != -1 {
+ p.currentScope.StrictMode = js_ast.ImplicitStrictModeJSXAutomaticRuntime
}
}
@@ -15907,7 +16202,7 @@ func (p *parser) computeCharacterFrequency() *js_ast.CharFreq {
func (p *parser) generateImportStmt(
path string,
imports []string,
- sourceIndex uint32,
+ sourceIndex *uint32,
parts []js_ast.Part,
symbols map[string]js_ast.Ref,
) []js_ast.Part {
@@ -15916,7 +16211,9 @@ func (p *parser) generateImportStmt(
declaredSymbols := make([]js_ast.DeclaredSymbol, len(imports))
clauseItems := make([]js_ast.ClauseItem, len(imports))
importRecordIndex := p.addImportRecord(ast.ImportStmt, logger.Loc{}, path, nil)
- p.importRecords[importRecordIndex].SourceIndex = ast.MakeIndex32(sourceIndex)
+ if sourceIndex != nil {
+ p.importRecords[importRecordIndex].SourceIndex = ast.MakeIndex32(*sourceIndex)
+ }
// Create per-import information
for i, alias := range imports {
@@ -15940,21 +16237,66 @@ func (p *parser) generateImportStmt(
NamespaceRef: namespaceRef,
Items: &clauseItems,
ImportRecordIndex: importRecordIndex,
+ IsSingleLine: true,
}}},
})
}
-func (p *parser) toAST(parts []js_ast.Part, hashbang string, directive string) js_ast.AST {
+// Sort the keys for determinism
+func sortedKeysOfMapStringRef(in map[string]js_ast.Ref) []string {
+ keys := make([]string, 0, len(in))
+ for key := range in {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, directive string) js_ast.AST {
// Insert an import statement for any runtime imports we generated
if len(p.runtimeImports) > 0 && !p.options.omitRuntimeForTests {
- // Sort the imports for determinism
- keys := make([]string, 0, len(p.runtimeImports))
- for key := range p.runtimeImports {
- keys = append(keys, key)
+ keys := sortedKeysOfMapStringRef(p.runtimeImports)
+ sourceIndex := runtime.SourceIndex
+ before = p.generateImportStmt("
", keys, &sourceIndex, before, p.runtimeImports)
+ }
+
+ // Insert an import statement for any jsx runtime imports we generated
+ if len(p.jsxRuntimeImports) > 0 && !p.options.omitJSXRuntimeForTests {
+ keys := sortedKeysOfMapStringRef(p.jsxRuntimeImports)
+
+ // Determine the runtime source and whether it's prod or dev
+ path := p.options.jsx.ImportSource
+ if p.options.jsx.Development {
+ path = path + "/jsx-dev-runtime"
+ } else {
+ path = path + "/jsx-runtime"
}
- sort.Strings(keys)
- parts = p.generateImportStmt("", keys, runtime.SourceIndex, parts, p.runtimeImports)
+
+ before = p.generateImportStmt(path, keys, nil, before, p.jsxRuntimeImports)
+ }
+
+ // Insert an import statement for any legacy jsx imports we generated (i.e., createElement)
+ if len(p.jsxLegacyImports) > 0 && !p.options.omitJSXRuntimeForTests {
+ keys := sortedKeysOfMapStringRef(p.jsxLegacyImports)
+ path := p.options.jsx.ImportSource
+ before = p.generateImportStmt(path, keys, nil, before, p.jsxLegacyImports)
+ }
+
+ // Generated imports are inserted before other code instead of appending them
+ // to the end of the file. Appending them should work fine because JavaScript
+ // import statements are "hoisted" to run before the importing file. However,
+ // some buggy JavaScript toolchains such as the TypeScript compiler convert
+ // ESM into CommonJS by replacing "import" statements inline without doing
+ // any hoisting, which is incorrect. See the following issue for more info:
+ // https://github.com/microsoft/TypeScript/issues/16166. Since JSX-related
+ // imports are present in the generated code when bundling is disabled, and
+ // could therefore be processed by these buggy tools, it's more robust to put
+ // them at the top even though it means potentially reallocating almost the
+ // entire array of parts.
+ if len(before) > 0 {
+ parts = append(before, parts...)
}
+ parts = append(parts, after...)
// Handle import paths after the whole file has been visited because we need
// symbol usage counts to be able to remove unused type-only imports in
diff --git a/internal/js_parser/js_parser_lower.go b/internal/js_parser/js_parser_lower.go
index ef1a4ab7b59..50daf17d34d 100644
--- a/internal/js_parser/js_parser_lower.go
+++ b/internal/js_parser/js_parser_lower.go
@@ -220,6 +220,13 @@ func (p *parser) markStrictModeFeature(feature strictModeFeature, r logger.Range
notes = []logger.MsgData{t.MsgData(tsAlwaysStrict.Range, fmt.Sprintf(
"TypeScript's %q setting was enabled here:", tsAlwaysStrict.Name))}
+ case js_ast.ImplicitStrictModeJSXAutomaticRuntime:
+ notes = []logger.MsgData{p.tracker.MsgData(logger.Range{Loc: p.firstJSXElementLoc, Len: 1},
+ "This file is implicitly in strict mode due to the JSX element here:"),
+ {Text: "When React's \"automatic\" JSX transform is enabled, using a JSX element automatically inserts " +
+ "an \"import\" statement at the top of the file for the corresponding the JSX helper function. " +
+ "This means the file is considered an ECMAScript module, and all ECMAScript modules use strict mode."}}
+
case js_ast.ExplicitStrictMode:
notes = []logger.MsgData{p.tracker.MsgData(p.source.RangeOfString(p.currentScope.UseStrictLoc),
"Strict mode is triggered by the \"use strict\" directive here:")}
diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go
index 975a7a5ee86..507b903b639 100644
--- a/internal/js_parser/js_parser_test.go
+++ b/internal/js_parser/js_parser_test.go
@@ -149,26 +149,34 @@ func expectPrintedJSX(t *testing.T, contents string, expected string) {
})
}
-func expectParseErrorTargetJSX(t *testing.T, esVersion int, contents string, expected string) {
+type JSXAutomaticTestOptions struct {
+ Development bool
+ ImportSource string
+ OmitJSXRuntimeForTests bool
+}
+
+func expectParseErrorJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, contents string, expected string) {
t.Helper()
expectParseErrorCommon(t, contents, expected, config.Options{
- UnsupportedJSFeatures: compat.UnsupportedJSFeatures(map[compat.Engine][]int{
- compat.ES: {esVersion},
- }),
+ OmitJSXRuntimeForTests: options.OmitJSXRuntimeForTests,
JSX: config.JSXOptions{
- Parse: true,
+ AutomaticRuntime: true,
+ Parse: true,
+ Development: options.Development,
+ ImportSource: options.ImportSource,
},
})
}
-func expectPrintedTargetJSX(t *testing.T, esVersion int, contents string, expected string) {
+func expectPrintedJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, contents string, expected string) {
t.Helper()
expectPrintedCommon(t, contents, expected, config.Options{
- UnsupportedJSFeatures: compat.UnsupportedJSFeatures(map[compat.Engine][]int{
- compat.ES: {esVersion},
- }),
+ OmitJSXRuntimeForTests: options.OmitJSXRuntimeForTests,
JSX: config.JSXOptions{
- Parse: true,
+ AutomaticRuntime: true,
+ Parse: true,
+ Development: options.Development,
+ ImportSource: options.ImportSource,
},
})
}
@@ -4718,6 +4726,160 @@ func TestJSXPragmas(t *testing.T) {
expectPrintedJSX(t, "/* @jsxFrag a.b.c */\n<>>", "/* @__PURE__ */ React.createElement(a.b.c, null);\n")
}
+func TestJSXAutomatic(t *testing.T) {
+ // Prod, without runtime imports
+ p := JSXAutomaticTestOptions{Development: false, OmitJSXRuntimeForTests: true}
+ expectPrintedJSXAutomatic(t, p, ">
", "/* @__PURE__ */ jsx(\"div\", {\n children: \">\"\n});\n")
+ expectPrintedJSXAutomatic(t, p, "{1}}
", "/* @__PURE__ */ jsxs(\"div\", {\n children: [\n 1,\n \"}\"\n ]\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"div\", {}, true);\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"div\", {}, \"key\");\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"div\", {\n ...props\n}, \"key\");\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n});\n") // Falls back to createElement
+ expectPrintedJSXAutomatic(t, p, "{...children}
", "/* @__PURE__ */ jsxs(\"div\", {\n children: [\n ...children\n ]\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsxs(\"div\", {\n children: [\n ...children,\n /* @__PURE__ */ jsx(\"a\", {})\n ]\n});\n")
+ expectPrintedJSXAutomatic(t, p, "<>>>", "/* @__PURE__ */ jsx(Fragment, {\n children: \">\"\n});\n")
+
+ expectParseErrorJSXAutomatic(t, p, "",
+ `: ERROR: Please provide an explicit value for "key":
+NOTE: Using "key" as a shorthand for "key={true}" is not allowed when using React's "automatic" JSX transform.
+`)
+ expectParseErrorJSXAutomatic(t, p, "",
+ `: ERROR: Duplicate "__self" prop found:
+NOTE: Both "__source" and "__self" are set automatically by esbuild when using React's "automatic" JSX transform. This duplicate prop may have come from a plugin.
+`)
+ expectParseErrorJSXAutomatic(t, p, "",
+ `: ERROR: Duplicate "__source" prop found:
+NOTE: Both "__source" and "__self" are set automatically by esbuild when using React's "automatic" JSX transform. This duplicate prop may have come from a plugin.
+`)
+
+ // Prod, with runtime imports
+ pr := JSXAutomaticTestOptions{Development: false}
+ expectPrintedJSXAutomatic(t, pr, "", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"div\", {});\n")
+ expectPrintedJSXAutomatic(t, pr, "<>>", "import { Fragment, jsx, jsxs } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsxs(Fragment, {\n children: [\n /* @__PURE__ */ jsx(\"a\", {}),\n /* @__PURE__ */ jsx(\"b\", {})\n ]\n});\n")
+ expectPrintedJSXAutomatic(t, pr, "", "import { createElement } from \"react\";\n/* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n});\n")
+ expectPrintedJSXAutomatic(t, pr, "<>>", "import { Fragment, jsx } from \"react/jsx-runtime\";\nimport { createElement } from \"react\";\n/* @__PURE__ */ jsx(Fragment, {\n children: /* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n })\n});\n")
+
+ pri := JSXAutomaticTestOptions{Development: false, ImportSource: "my-jsx-lib"}
+ expectPrintedJSXAutomatic(t, pri, "", "import { jsx } from \"my-jsx-lib/jsx-runtime\";\n/* @__PURE__ */ jsx(\"div\", {});\n")
+ expectPrintedJSXAutomatic(t, pri, "", "import { createElement } from \"my-jsx-lib\";\n/* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n});\n")
+
+ // Dev, without runtime imports
+ d := JSXAutomaticTestOptions{Development: true, OmitJSXRuntimeForTests: true}
+ expectPrintedJSXAutomatic(t, d, ">
", "/* @__PURE__ */ jsxDEV(\"div\", {\n children: \">\"\n}, void 0, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "{1}}
", "/* @__PURE__ */ jsxDEV(\"div\", {\n children: [\n 1,\n \"}\"\n ]\n}, void 0, true, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "", "/* @__PURE__ */ jsxDEV(\"div\", {}, true, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "", "/* @__PURE__ */ jsxDEV(\"div\", {}, \"key\", false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "", "/* @__PURE__ */ jsxDEV(\"div\", {\n ...props\n}, \"key\", false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "", "/* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n});\n") // Falls back to createElement
+ expectPrintedJSXAutomatic(t, d, "{...children}
", "/* @__PURE__ */ jsxDEV(\"div\", {\n children: [\n ...children\n ]\n}, void 0, true, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "", "/* @__PURE__ */ jsxDEV(\"div\", {\n children: [\n ...children,\n /* @__PURE__ */ jsxDEV(\"a\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 3,\n columnNumber: 3\n }, this)\n ]\n}, void 0, true, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "<>>>", "/* @__PURE__ */ jsxDEV(Fragment, {\n children: \">\"\n}, void 0, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+
+ expectParseErrorJSXAutomatic(t, d, "",
+ `: ERROR: Please provide an explicit value for "key":
+NOTE: Using "key" as a shorthand for "key={true}" is not allowed when using React's "automatic" JSX transform.
+`)
+ expectParseErrorJSXAutomatic(t, d, "",
+ `: ERROR: Duplicate "__self" prop found:
+NOTE: Both "__source" and "__self" are set automatically by esbuild when using React's "automatic" JSX transform. This duplicate prop may have come from a plugin.
+`)
+ expectParseErrorJSXAutomatic(t, d, "",
+ `: ERROR: Duplicate "__source" prop found:
+NOTE: Both "__source" and "__self" are set automatically by esbuild when using React's "automatic" JSX transform. This duplicate prop may have come from a plugin.
+`)
+
+ // Line/column offset tests. Unlike Babel, TypeScript sometimes points to a
+ // location other than the start of the element. I'm not sure if that's a bug
+ // or not, but it seems weird. So I decided to match Babel instead.
+ expectPrintedJSXAutomatic(t, d, "\r\n", "/* @__PURE__ */ jsxDEV(\"x\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 2,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "\n\r", "/* @__PURE__ */ jsxDEV(\"x\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 3,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, d, "let 𐀀 = 🍕🍕🍕", "let 𐀀 = /* @__PURE__ */ jsxDEV(\"x\", {\n children: [\n \"🍕🍕🍕\",\n /* @__PURE__ */ jsxDEV(\"y\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 19\n }, this)\n ]\n}, void 0, true, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 10\n}, this);\n")
+
+ // Dev, with runtime imports
+ dr := JSXAutomaticTestOptions{Development: true}
+ expectPrintedJSXAutomatic(t, dr, "", "import { jsxDEV } from \"react/jsx-dev-runtime\";\n/* @__PURE__ */ jsxDEV(\"div\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, dr, "<>\n \n \n>", "import { Fragment, jsxDEV } from \"react/jsx-dev-runtime\";\n/* @__PURE__ */ jsxDEV(Fragment, {\n children: [\n /* @__PURE__ */ jsxDEV(\"a\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 2,\n columnNumber: 3\n }, this),\n /* @__PURE__ */ jsxDEV(\"b\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 3,\n columnNumber: 3\n }, this)\n ]\n}, void 0, true, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+
+ dri := JSXAutomaticTestOptions{Development: true, ImportSource: "preact"}
+ expectPrintedJSXAutomatic(t, dri, "", "import { jsxDEV } from \"preact/jsx-dev-runtime\";\n/* @__PURE__ */ jsxDEV(\"div\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+ expectPrintedJSXAutomatic(t, dri, "<>\n \n \n>", "import { Fragment, jsxDEV } from \"preact/jsx-dev-runtime\";\n/* @__PURE__ */ jsxDEV(Fragment, {\n children: [\n /* @__PURE__ */ jsxDEV(\"a\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 2,\n columnNumber: 3\n }, this),\n /* @__PURE__ */ jsxDEV(\"b\", {}, void 0, false, {\n fileName: \"\",\n lineNumber: 3,\n columnNumber: 3\n }, this)\n ]\n}, void 0, true, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
+
+ // JSX namespaced names
+ for _, colon := range []string{":", " :", ": ", " : "} {
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"a:b\", {});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"a-b:c-d\", {});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"a-:b-\", {});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"Te:st\", {});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"a:b\": true\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"a-b:c-d\": true\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"a-:b-\": true\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"Te:st\": true\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"a:b\": 0\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"a-b:c-d\": 0\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"a-:b-\": 0\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"x\", {\n \"Te:st\": 0\n});\n")
+ expectPrintedJSXAutomatic(t, p, "", "/* @__PURE__ */ jsx(\"a-b\", {\n \"a-b\": a - b\n});\n")
+ expectParseErrorJSXAutomatic(t, p, "", ": ERROR: Expected identifier after \"x:\" in namespaced JSX name\n")
+ expectParseErrorJSXAutomatic(t, p, "", ": ERROR: Expected \">\" but found \":\"\n")
+ expectParseErrorJSXAutomatic(t, p, "", ": ERROR: Expected identifier after \"x:\" in namespaced JSX name\n")
+ }
+
+ // Enabling the "automatic" runtime means that any JSX element will cause the
+ // file to be implicitly in strict mode due to the automatically-generated
+ // import statement. This is the same behavior as the TypeScript compiler.
+ strictModeError := ": ERROR: With statements cannot be used in strict mode\n" +
+ ": NOTE: This file is implicitly in strict mode due to the JSX element here:\n" +
+ "NOTE: When React's \"automatic\" JSX transform is enabled, using a JSX element automatically inserts an \"import\" statement at the top of the file " +
+ "for the corresponding the JSX helper function. This means the file is considered an ECMAScript module, and all ECMAScript modules use strict mode.\n"
+ expectPrintedJSX(t, "with (x) y()", "with (x)\n y(/* @__PURE__ */ React.createElement(\"z\", null));\n")
+ expectPrintedJSXAutomatic(t, p, "with (x) y", "with (x)\n y;\n")
+ expectParseErrorJSX(t, "with (x) y() // @jsxRuntime automatic", strictModeError)
+ expectParseErrorJSXAutomatic(t, p, "with (x) y()", strictModeError)
+}
+
+func TestJSXAutomaticPragmas(t *testing.T) {
+ expectPrintedJSX(t, "// @jsxRuntime automatic\n", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "/*@jsxRuntime automatic*/\n", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "/* @jsxRuntime automatic */\n", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "\n/*@jsxRuntime automatic*/", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "\n/* @jsxRuntime automatic */", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+
+ expectPrintedJSX(t, "// @jsxRuntime classic\n", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
+ expectPrintedJSX(t, "/*@jsxRuntime classic*/\n", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
+ expectPrintedJSX(t, "/* @jsxRuntime classic */\n", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
+ expectPrintedJSX(t, "\n/*@jsxRuntime classic*/\n", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
+ expectPrintedJSX(t, "\n/* @jsxRuntime classic */\n", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
+
+ expectParseErrorJSX(t, "// @jsxRuntime foo\n",
+ `: WARNING: Invalid JSX runtime: "foo"
+NOTE: The JSX runtime can only be set to either "classic" or "automatic".
+`)
+
+ expectPrintedJSX(t, "// @jsxRuntime automatic @jsxImportSource src\n", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "/*@jsxRuntime automatic @jsxImportSource src*/\n", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "/*@jsxRuntime automatic*//*@jsxImportSource src*/\n", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "/* @jsxRuntime automatic */\n/* @jsxImportSource src */\n", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "\n/*@jsxRuntime automatic @jsxImportSource src*/", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "\n/*@jsxRuntime automatic*/\n/*@jsxImportSource src*/", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectPrintedJSX(t, "\n/* @jsxRuntime automatic */\n/* @jsxImportSource src */", "import { jsx } from \"src/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+
+ expectPrintedJSX(t, "// @jsxRuntime classic @jsxImportSource src\n", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
+ expectParseErrorJSX(t, "// @jsxRuntime classic @jsxImportSource src\n",
+ `: WARNING: The JSX import source cannot be set without also enabling React's "automatic" JSX transform
+NOTE: You can enable React's "automatic" JSX transform for this file by using a "@jsxRuntime automatic" comment.
+`)
+ expectParseErrorJSX(t, "// @jsxImportSource src\n",
+ `: WARNING: The JSX import source cannot be set without also enabling React's "automatic" JSX transform
+NOTE: You can enable React's "automatic" JSX transform for this file by using a "@jsxRuntime automatic" comment.
+`)
+
+ expectPrintedJSX(t, "// @jsxRuntime automatic @jsx h\n", "import { jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(\"a\", {});\n")
+ expectParseErrorJSX(t, "// @jsxRuntime automatic @jsx h\n", ": WARNING: The JSX factory cannot be set when using React's \"automatic\" JSX transform\n")
+
+ expectPrintedJSX(t, "// @jsxRuntime automatic @jsxFrag f\n<>>", "import { Fragment, jsx } from \"react/jsx-runtime\";\n/* @__PURE__ */ jsx(Fragment, {});\n")
+ expectParseErrorJSX(t, "// @jsxRuntime automatic @jsxFrag f\n<>>", ": WARNING: The JSX fragment cannot be set when using React's \"automatic\" JSX transform\n")
+}
+
func TestPreserveOptionalChainParentheses(t *testing.T) {
expectPrinted(t, "a?.b.c", "a?.b.c;\n")
expectPrinted(t, "(a?.b).c", "(a?.b).c;\n")
diff --git a/internal/js_printer/js_printer_test.go b/internal/js_printer/js_printer_test.go
index e49c1cb4319..66acda27a89 100644
--- a/internal/js_printer/js_printer_test.go
+++ b/internal/js_printer/js_printer_test.go
@@ -953,15 +953,15 @@ func TestAvoidSlashScript(t *testing.T) {
expectPrinted(t, "/*! \";\n")
+ "import { __template } from \"\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/script\"])));\n")
expectPrinted(t, "String.raw`\";\n")
+ "import { __template } from \"\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/script\", \"\"])), a);\n")
expectPrinted(t, "String.raw`${a}\";\n")
+ "import { __template } from \"\";\nvar _a;\nString.raw(_a || (_a = __template([\"\", \"<\\/script\"])), a);\n")
expectPrinted(t, "String.raw`\";\n")
+ "import { __template } from \"\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/SCRIPT\"])));\n")
expectPrinted(t, "String.raw`\";\n")
+ "import { __template } from \"\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/ScRiPt\"])));\n")
// Negative cases
expectPrinted(t, "x = ''", "x = \"\";\n")
diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go
index 9649e42a46f..551c0f8c065 100644
--- a/internal/resolver/resolver.go
+++ b/internal/resolver/resolver.go
@@ -101,8 +101,10 @@ type ResolveResult struct {
PluginData interface{}
// If not empty, these should override the default values
- JSXFactory []string // Default if empty: "React.createElement"
- JSXFragment []string // Default if empty: "React.Fragment"
+ JSXFactory []string // Default if empty: "React.createElement"
+ JSXFragment []string // Default if empty: "React.Fragment"
+ JSXImportSource string // Default if empty: "react"
+ JSX config.TSJSX
DifferentCase *fs.DifferentCase
@@ -626,6 +628,8 @@ func (r resolverQuery) finalizeResolve(result *ResolveResult) {
} else {
result.JSXFactory = dirInfo.enclosingTSConfigJSON.JSXFactory
result.JSXFragment = dirInfo.enclosingTSConfigJSON.JSXFragmentFactory
+ result.JSX = dirInfo.enclosingTSConfigJSON.JSX
+ result.JSXImportSource = dirInfo.enclosingTSConfigJSON.JSXImportSource
result.UseDefineForClassFieldsTS = dirInfo.enclosingTSConfigJSON.UseDefineForClassFields
result.UnusedImportFlagsTS = config.UnusedImportFlagsFromTsconfigValues(
dirInfo.enclosingTSConfigJSON.PreserveImportsNotUsedAsValues,
diff --git a/internal/resolver/tsconfig_json.go b/internal/resolver/tsconfig_json.go
index ff2292b0fb0..a47eadb1eb7 100644
--- a/internal/resolver/tsconfig_json.go
+++ b/internal/resolver/tsconfig_json.go
@@ -38,8 +38,10 @@ type TSConfigJSON struct {
TSTarget *config.TSTarget
TSStrict *config.TSAlwaysStrict
TSAlwaysStrict *config.TSAlwaysStrict
+ JSX config.TSJSX
JSXFactory []string
JSXFragmentFactory []string
+ JSXImportSource string
ModuleSuffixes []string
UseDefineForClassFields config.MaybeBool
PreserveImportsNotUsedAsValues bool
@@ -105,6 +107,24 @@ func ParseTSConfigJSON(
}
}
+ // Parse "jsx"
+ if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsx"); ok {
+ if value, ok := getString(valueJSON); ok {
+ switch strings.ToLower(value) {
+ case "none":
+ result.JSX = config.TSJSXNone
+ case "preserve", "react-native":
+ result.JSX = config.TSJSXPreserve
+ case "react":
+ result.JSX = config.TSJSXReact
+ case "react-jsx":
+ result.JSX = config.TSJSXReactJSX
+ case "react-jsxdev":
+ result.JSX = config.TSJSXReactJSXDev
+ }
+ }
+ }
+
// Parse "jsxFactory"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFactory"); ok {
if value, ok := getString(valueJSON); ok {
@@ -119,6 +139,13 @@ func ParseTSConfigJSON(
}
}
+ // Parse "jsxImportSource"
+ if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxImportSource"); ok {
+ if value, ok := getString(valueJSON); ok {
+ result.JSXImportSource = value
+ }
+ }
+
// Parse "moduleSuffixes"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "moduleSuffixes"); ok {
if value, ok := valueJSON.Data.(*js_ast.EArray); ok {
diff --git a/lib/shared/common.ts b/lib/shared/common.ts
index 37f8adeff1d..662f43107de 100644
--- a/lib/shared/common.ts
+++ b/lib/shared/common.ts
@@ -140,6 +140,8 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
let jsx = getFlag(options, keys, 'jsx', mustBeString);
let jsxFactory = getFlag(options, keys, 'jsxFactory', mustBeString);
let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString);
+ let jsxImportSource = getFlag(options, keys, 'jsxImportSource', mustBeString);
+ let jsxDev = getFlag(options, keys, 'jsxDev', mustBeBoolean);
let define = getFlag(options, keys, 'define', mustBeObject);
let logOverride = getFlag(options, keys, 'logOverride', mustBeObject);
let supported = getFlag(options, keys, 'supported', mustBeObject);
@@ -173,6 +175,8 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
if (jsx) flags.push(`--jsx=${jsx}`);
if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`);
if (jsxFragment) flags.push(`--jsx-fragment=${jsxFragment}`);
+ if (jsxImportSource) flags.push(`--jsx-import-source=${jsxImportSource}`);
+ if (jsxDev) flags.push(`--jsx-dev`);
if (define) {
for (let key in define) {
diff --git a/lib/shared/types.ts b/lib/shared/types.ts
index a08c0dc6599..31a5d0ba60a 100644
--- a/lib/shared/types.ts
+++ b/lib/shared/types.ts
@@ -52,11 +52,15 @@ interface CommonOptions {
ignoreAnnotations?: boolean;
/** Documentation: https://esbuild.github.io/api/#jsx */
- jsx?: 'transform' | 'preserve';
+ jsx?: 'transform' | 'preserve' | 'automatic';
/** Documentation: https://esbuild.github.io/api/#jsx-factory */
jsxFactory?: string;
/** Documentation: https://esbuild.github.io/api/#jsx-fragment */
jsxFragment?: string;
+ /** Documentation: https://esbuild.github.io/api/#jsx-import-source */
+ jsxImportSource?: string;
+ /** Documentation: https://esbuild.github.io/api/#jsx-development */
+ jsxDev?: boolean;
/** Documentation: https://esbuild.github.io/api/#define */
define?: { [key: string]: string };
diff --git a/pkg/api/api.go b/pkg/api/api.go
index ee3265b571f..8d5745e44f5 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -110,6 +110,7 @@ type JSXMode uint8
const (
JSXModeTransform JSXMode = iota
JSXModePreserve
+ JSXModeAutomatic
)
type Target uint8
@@ -276,9 +277,11 @@ type BuildOptions struct {
IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations
LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments
- JSXMode JSXMode // Documentation: https://esbuild.github.io/api/#jsx-mode
- JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory
- JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
+ JSXMode JSXMode // Documentation: https://esbuild.github.io/api/#jsx-mode
+ JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory
+ JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
+ JSXImportSource string // Documentation: https://esbuild.github.io/api/#jsx-import-source
+ JSXDev bool // Documentation: https://esbuild.github.io/api/#jsx-dev
Define map[string]string // Documentation: https://esbuild.github.io/api/#define
Pure []string // Documentation: https://esbuild.github.io/api/#pure
@@ -396,9 +399,11 @@ type TransformOptions struct {
IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations
LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments
- JSXMode JSXMode // Documentation: https://esbuild.github.io/api/#jsx
- JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory
- JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
+ JSXMode JSXMode // Documentation: https://esbuild.github.io/api/#jsx
+ JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory
+ JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
+ JSXImportSource string // Documentation: https://esbuild.github.io/api/#jsx-import-source
+ JSXDev bool // Documentation: https://esbuild.github.io/api/#jsx-dev
TsconfigRaw string // Documentation: https://esbuild.github.io/api/#tsconfig-raw
Banner string // Documentation: https://esbuild.github.io/api/#banner
diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go
index 9a59286cf75..12fdc23c313 100644
--- a/pkg/api/api_impl.go
+++ b/pkg/api/api_impl.go
@@ -897,9 +897,12 @@ func rebuildImpl(
UnsupportedCSSFeatureOverridesMask: cssMask,
OriginalTargetEnv: targetEnv,
JSX: config.JSXOptions{
- Preserve: buildOpts.JSXMode == JSXModePreserve,
- Factory: validateJSXExpr(log, buildOpts.JSXFactory, "factory"),
- Fragment: validateJSXExpr(log, buildOpts.JSXFragment, "fragment"),
+ Preserve: buildOpts.JSXMode == JSXModePreserve,
+ AutomaticRuntime: buildOpts.JSXMode == JSXModeAutomatic,
+ Factory: validateJSXExpr(log, buildOpts.JSXFactory, "factory"),
+ Fragment: validateJSXExpr(log, buildOpts.JSXFragment, "fragment"),
+ Development: buildOpts.JSXDev,
+ ImportSource: buildOpts.JSXImportSource,
},
Defines: defines,
InjectedDefines: injectedDefines,
@@ -1356,9 +1359,12 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
var unusedImportFlagsTS config.UnusedImportFlagsTS
useDefineForClassFieldsTS := config.Unspecified
jsx := config.JSXOptions{
- Preserve: transformOpts.JSXMode == JSXModePreserve,
- Factory: validateJSXExpr(log, transformOpts.JSXFactory, "factory"),
- Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"),
+ Preserve: transformOpts.JSXMode == JSXModePreserve,
+ AutomaticRuntime: transformOpts.JSXMode == JSXModeAutomatic,
+ Factory: validateJSXExpr(log, transformOpts.JSXFactory, "factory"),
+ Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"),
+ Development: transformOpts.JSXDev,
+ ImportSource: transformOpts.JSXImportSource,
}
// Settings from "tsconfig.json" override those
@@ -1372,12 +1378,18 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
Contents: transformOpts.TsconfigRaw,
}
if result := resolver.ParseTSConfigJSON(log, source, &caches.JSONCache, nil); result != nil {
+ if result.JSX != config.TSJSXNone {
+ jsx.SetOptionsFromTSJSX(result.JSX)
+ }
if len(result.JSXFactory) > 0 {
jsx.Factory = config.DefineExpr{Parts: result.JSXFactory}
}
if len(result.JSXFragmentFactory) > 0 {
jsx.Fragment = config.DefineExpr{Parts: result.JSXFragmentFactory}
}
+ if len(result.JSXImportSource) > 0 {
+ jsx.ImportSource = result.JSXImportSource
+ }
if result.UseDefineForClassFields != config.Unspecified {
useDefineForClassFieldsTS = result.UseDefineForClassFields
}
diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go
index 00375d8f3bf..08064021362 100644
--- a/pkg/cli/cli_impl.go
+++ b/pkg/cli/cli_impl.go
@@ -610,10 +610,12 @@ func parseOptionsImpl(
mode = api.JSXModeTransform
case "preserve":
mode = api.JSXModePreserve
+ case "automatic":
+ mode = api.JSXModeAutomatic
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
- "Valid values are \"transform\" or \"preserve\".",
+ "Valid values are \"transform\", \"automatic\", or \"preserve\".",
)
}
if buildOpts != nil {
@@ -638,6 +640,23 @@ func parseOptionsImpl(
transformOpts.JSXFragment = value
}
+ case strings.HasPrefix(arg, "--jsx-import-source="):
+ value := arg[len("--jsx-import-source="):]
+ if buildOpts != nil {
+ buildOpts.JSXImportSource = value
+ } else {
+ transformOpts.JSXImportSource = value
+ }
+
+ case isBoolFlag(arg, "--jsx-dev"):
+ if value, err := parseBoolFlag(arg, true); err != nil {
+ return parseOptionsExtras{}, err
+ } else if buildOpts != nil {
+ buildOpts.JSXDev = value
+ } else {
+ transformOpts.JSXDev = value
+ }
+
case strings.HasPrefix(arg, "--banner=") && transformOpts != nil:
transformOpts.Banner = arg[len("--banner="):]
@@ -734,6 +753,7 @@ func parseOptionsImpl(
"allow-overwrite": true,
"bundle": true,
"ignore-annotations": true,
+ "jsx-dev": true,
"keep-names": true,
"minify-identifiers": true,
"minify-syntax": true,
@@ -761,6 +781,7 @@ func parseOptionsImpl(
"ignore-annotations": true,
"jsx-factory": true,
"jsx-fragment": true,
+ "jsx-import-source": true,
"jsx": true,
"keep-names": true,
"legal-comments": true,
diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js
index 8420b515c32..f6eac4ff2dc 100644
--- a/scripts/js-api-tests.js
+++ b/scripts/js-api-tests.js
@@ -3526,6 +3526,37 @@ let transformTests = {
loader: 'jsx',
})
assert.strictEqual(code2, `/* @__PURE__ */ factory(fragment, null, /* @__PURE__ */ factory("div", null));\n`)
+
+ const { code: code3 } = await esbuild.transform(`<>>`, {
+ tsconfigRaw: {
+ compilerOptions: {
+ jsx: 'react-jsx'
+ },
+ },
+ loader: 'jsx',
+ })
+ assert.strictEqual(code3, `import { Fragment, jsx } from "react/jsx-runtime";\n/* @__PURE__ */ jsx(Fragment, {\n children: /* @__PURE__ */ jsx("div", {})\n});\n`)
+
+ const { code: code4 } = await esbuild.transform(`<>>`, {
+ tsconfigRaw: {
+ compilerOptions: {
+ jsx: 'react-jsx',
+ jsxImportSource: 'notreact'
+ },
+ },
+ loader: 'jsx',
+ })
+ assert.strictEqual(code4, `import { Fragment, jsx } from "notreact/jsx-runtime";\n/* @__PURE__ */ jsx(Fragment, {\n children: /* @__PURE__ */ jsx("div", {})\n});\n`)
+
+ const { code: code5 } = await esbuild.transform(`<>>`, {
+ tsconfigRaw: {
+ compilerOptions: {
+ jsx: 'react-jsxdev'
+ },
+ },
+ loader: 'jsx',
+ })
+ assert.strictEqual(code5, `import { Fragment, jsxDEV } from "react/jsx-dev-runtime";\n/* @__PURE__ */ jsxDEV(Fragment, {\n children: /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {\n fileName: "",\n lineNumber: 1,\n columnNumber: 3\n }, this)\n}, void 0, false, {\n fileName: "",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n`)
},
// Note: tree shaking is disabled when the output format isn't IIFE
@@ -3883,6 +3914,21 @@ let transformTests = {
assert.strictEqual(code, `console.log();\n`)
},
+ async jsxRuntimeAutomatic({ esbuild }) {
+ const { code } = await esbuild.transform(`console.log()`, { loader: 'jsx', jsx: 'automatic' })
+ assert.strictEqual(code, `import { jsx } from "react/jsx-runtime";\nconsole.log(/* @__PURE__ */ jsx("div", {}));\n`)
+ },
+
+ async jsxDev({ esbuild }) {
+ const { code } = await esbuild.transform(`console.log()`, { loader: 'jsx', jsx: 'automatic', jsxDev: true })
+ assert.strictEqual(code, `import { jsxDEV } from "react/jsx-dev-runtime";\nconsole.log(/* @__PURE__ */ jsxDEV("div", {}, void 0, false, {\n fileName: "",\n lineNumber: 1,\n columnNumber: 13\n}, this));\n`)
+ },
+
+ async jsxImportSource({ esbuild }) {
+ const { code } = await esbuild.transform(`console.log()`, { loader: 'jsx', jsx: 'automatic', jsxImportSource: 'notreact' })
+ assert.strictEqual(code, `import { jsx } from "notreact/jsx-runtime";\nconsole.log(/* @__PURE__ */ jsx("div", {}));\n`)
+ },
+
async ts({ esbuild }) {
const { code } = await esbuild.transform(`enum Foo { FOO }`, { loader: 'ts' })
assert.strictEqual(code, `var Foo = /* @__PURE__ */ ((Foo2) => {\n Foo2[Foo2["FOO"] = 0] = "FOO";\n return Foo2;\n})(Foo || {});\n`)
diff --git a/scripts/verify-source-map.js b/scripts/verify-source-map.js
index 270d1076ca6..c1c138f9339 100644
--- a/scripts/verify-source-map.js
+++ b/scripts/verify-source-map.js
@@ -320,6 +320,33 @@ const testCaseBundleCSS = {
`,
}
+const testCaseJSXRuntime = {
+ 'entry.jsx': `
+ import { A0, A1, A2 } from './a.jsx';
+ console.log()
+ `,
+ 'a.jsx': `
+ import {jsx} from './b-dir/b'
+ import {Fragment} from './b-dir/c-dir/c'
+ export function A0() { return <>a0> }
+ export function A1() { return a1
}
+ export function A2() { return }
+ `,
+ 'b-dir/b.js': `
+ export const jsx = {id: 'jsx'}
+ `,
+ 'b-dir/c-dir/c.jsx': `
+ exports.Fragment = function() { return <>> }
+ `,
+}
+
+const toSearchJSXRuntime = {
+ A0: 'a.jsx',
+ A1: 'a.jsx',
+ A2: 'a.jsx',
+ jsx: 'b-dir/b.js',
+}
+
const testCaseNames = {
'entry.js': `
import "./nested1"
@@ -759,6 +786,18 @@ async function main() {
entryPoints: ['entry.css'],
crlf,
}),
+ check('jsx-runtime' + suffix, testCaseJSXRuntime, toSearchJSXRuntime, {
+ ext: 'js',
+ flags: flags.concat('--outfile=out.js', '--bundle', '--jsx=automatic', '--external:react/jsx-runtime'),
+ entryPoints: ['entry.jsx'],
+ crlf,
+ }),
+ check('jsx-dev-runtime' + suffix, testCaseJSXRuntime, toSearchJSXRuntime, {
+ ext: 'js',
+ flags: flags.concat('--outfile=out.js', '--bundle', '--jsx=automatic', '--jsx-dev', '--external:react/jsx-dev-runtime'),
+ entryPoints: ['entry.jsx'],
+ crlf,
+ }),
// Checks for the "names" field
checkNames('names' + suffix, testCaseNames, {