From 013722433bb4a0c8f7e91ec82e5ab6c9feb14284 Mon Sep 17 00:00:00 2001 From: John Gozde Date: Thu, 28 Jul 2022 09:20:17 -0600 Subject: [PATCH] fix #334: support automatic JSX runtime (#2349) --- CHANGELOG.md | 55 +++ cmd/esbuild/main.go | 6 +- internal/bundler/bundler.go | 6 + internal/bundler/bundler_default_test.go | 82 ++++ internal/bundler/bundler_tsconfig_test.go | 85 ++++ .../bundler/snapshots/snapshots_default.txt | 36 ++ .../bundler/snapshots/snapshots_tsconfig.txt | 60 +++ internal/config/config.go | 38 +- internal/js_ast/js_ast.go | 1 + internal/js_lexer/js_lexer.go | 28 +- internal/js_parser/js_parser.go | 440 ++++++++++++++++-- internal/js_parser/js_parser_lower.go | 7 + internal/js_parser/js_parser_test.go | 182 +++++++- internal/js_printer/js_printer_test.go | 10 +- internal/resolver/resolver.go | 8 +- internal/resolver/tsconfig_json.go | 27 ++ lib/shared/common.ts | 4 + lib/shared/types.ts | 6 +- pkg/api/api.go | 17 +- pkg/api/api_impl.go | 24 +- pkg/cli/cli_impl.go | 23 +- scripts/js-api-tests.js | 46 ++ scripts/verify-source-map.js | 39 ++ 23 files changed, 1136 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e23d141fd..ea475536c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # Changelog +## Unreleased + +* Add support for React 17's `automatic` JSX transform ([#334](https://github.com/evanw/esbuild/issues/334), [#718](https://github.com/evanw/esbuild/issues/718), [#1172](https://github.com/evanw/esbuild/issues/1172), [#2318](https://github.com/evanw/esbuild/issues/2318), [#334](https://github.com/evanw/esbuild/pull/2349)) + + This adds support for the [new "automatic" JSX runtime from React 17+](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) to esbuild for both the build and transform APIs. + + **New CLI flags and API options:** + - `--jsx`, `jsx` — Set this to `"automatic"` to opt in to this new transform + - `--jsx-dev`, `jsxDev` — Toggles development mode for the automatic runtime + - `--jsx-import-source`, `jsxImportSource` — Overrides the root import for runtime functions (default `"react"`) + + **New JSX pragma comments:** + - `@jsxRuntime` — Sets the runtime (`automatic` or `classic`) + - `@jsxImportSource` — Sets the import source (only valid with automatic runtime) + + The existing `@jsxFragment` and `@jsxFactory` pragma comments are only valid with "classic" runtime. + + **TSConfig resolving:** + Along with accepting the new options directly via CLI or API, option inference from `tsconfig.json` compiler options was also implemented: + + - `"jsx": "preserve"` or `"jsx": "react-native"` → Same as `--jsx=preserve` in esbuild + - `"jsx": "react"` → Same as `--jsx=transform` in esbuild (which is the default behavior) + - `"jsx": "react-jsx"` → Same as `--jsx=automatic` in esbuild + - `"jsx": "react-jsxdev"` → Same as `--jsx=automatic --jsx-dev` in esbuild + + It also reads the value of `"jsxImportSource"` from `tsconfig.json` if specified. + + For `react-jsx` it's important to note that it doesn't implicitly disable `--jsx-dev`. This is to support the case where a user sets `"react-jsx"` in their `tsconfig.json` but then toggles development mode directly in esbuild. + + **esbuild vs Babel vs TS vs...** + + There are a few differences between the various technologies that implement automatic JSX runtimes. The JSX transform in esbuild follows a mix of Babel's and TypeScript's behavior: + + - When an element has `__source` or `__self` props: + - Babel: Print an error about a deprecated transform plugin + - TypeScript: Allow the props + - swc: Hard crash + - **esbuild**: Print an error — Following Babel was chosen for this one because this might help people catch configuration issues where JSX files are being parsed by multiple tools + + - Element has an "implicit true" key prop, e.g. ``: + - Babel: Print an error indicating that "key" props require an explicit value + - TypeScript: Silently omit the "key" prop + - swc: Hard crash + - **esbuild**: Print an error like Babel — This might help catch legitimate programming mistakes + + - Element has spread children, e.g. `{...children}` + - Babel: Print an error stating that React doesn't support spread children + - TypeScript: Use static jsx function and pass children as-is, including spread operator + - swc: same as Babel + - **esbuild**: Same as TypeScript + + Also note that TypeScript has some bugs regarding JSX development mode and the generation of `lineNumber` and `columnNumber` values. Babel's values are accurate though, so esbuild's line and column numbers match Babel. Both numbers are 1-based and columns are counted in terms of UTF-16 code units. + + This feature was contributed by [@jgoz](https://github.com/jgoz). + ## 0.14.50 * Emit `names` in source maps ([#1296](https://github.com/evanw/esbuild/issues/1296)) diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index ac9d85fd3b7..6f9dc823a9a 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -77,9 +77,13 @@ var helpText = func(colors logger.Colors) string { incorrect tree-shaking annotations --inject:F Import the file F into all input files and automatically replace matching globals with imports + --jsx-dev Use React's automatic runtime in development mode --jsx-factory=... What to use for JSX instead of React.createElement --jsx-fragment=... What to use for JSX instead of React.Fragment - --jsx=... Set to "preserve" to disable transforming JSX to JS + --jsx-import-source=... Override the package name for the automatic runtime + (default "react") + --jsx=... Set to "automatic" to use React's automatic runtime + or to "preserve" to disable transforming JSX to JS --keep-names Preserve "name" on functions and classes --legal-comments=... Where to place legal comments (none | inline | eof | linked | external, default eof when bundling diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 2c44924afea..4c3c2c61c0a 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -1194,6 +1194,12 @@ func (s *scanner) maybeParseFile( if len(resolveResult.JSXFragment) > 0 { optionsClone.JSX.Fragment = config.DefineExpr{Parts: resolveResult.JSXFragment} } + if resolveResult.JSX != config.TSJSXNone { + optionsClone.JSX.SetOptionsFromTSJSX(resolveResult.JSX) + } + if resolveResult.JSXImportSource != "" { + optionsClone.JSX.ImportSource = resolveResult.JSXImportSource + } if resolveResult.UseDefineForClassFieldsTS != config.Unspecified { optionsClone.UseDefineForClassFields = resolveResult.UseDefineForClassFieldsTS } diff --git a/internal/bundler/bundler_default_test.go b/internal/bundler/bundler_default_test.go index bca4deefe2c..465695864ae 100644 --- a/internal/bundler/bundler_default_test.go +++ b/internal/bundler/bundler_default_test.go @@ -539,6 +539,88 @@ func TestJSXConstantFragments(t *testing.T) { }) } +func TestJSXAutomaticImportsCommonJS(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": ` + 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, "
{...children}
", "/* @__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, "
\n {...children}\n
", "/* @__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 = ' 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, {