Repository: gohugoio/hugo (language: go)

Issue:
Support ESBuild options `JSXImportSource` and `JSX`
Hugo already has support for passing the `JSXFactory` and `JSXFragment` options to ESBuild:

https://github.com/gohugoio/hugo/blob/16406d9d77cb4861bba9df8ca39e7dadfe41eb45/resources/resource_transformers/js/options.go#L416-L417

These are unfortunately pretty unergonomic when using Preact. If I use the options `"JSXFactory" "h" "JSXFragment" "Fragment"`, I have to prepend an (otherwise unused) `import { h, Fragment } from 'preact';` before every JSX file.

It would probably be possible to work around that using `inject`, but ESBuild has two newer options that don't require such workarounds anymore, `JSXImportSource` and `JSX`: https://esbuild.github.io/api/#jsx-import-source

I would appreciate if it was possible to set these through Hugo. Currently, the only way I've seen to set these would be on a per-file level like this:

```js
// @jsxImportSource preact
// @jsxRuntime automatic
```

If there is interest in supporting those two options, I'm happy to submit a PR. Thanks!


Discussion / hints:
>I'm happy to submit a PR. Thanks!

Please do.

Fix patch:
diff --git a/docs/content/en/functions/js/Build.md b/docs/content/en/functions/js/Build.md
index 83578548614..de9097a676a 100644
--- a/docs/content/en/functions/js/Build.md
+++ b/docs/content/en/functions/js/Build.md
@@ -111,6 +111,29 @@ format
 sourceMap
 : (`string`) Whether to generate `inline` or `external` source maps from esbuild. External source maps will be written to the target with the output file name + ".map". Input source maps can be read from js.Build and node modules and combined into the output source maps. By default, source maps are not created.
 
+JSX {{< new-in 0.124.0 >}}
+: (`string`) How to handle/transform JSX syntax. One of: `transform`, `preserve`, `automatic`. Default is `transform`. Notably, the `automatic` transform was introduced in React 17+ and will cause the necessary JSX helper functions to be imported automatically. See https://esbuild.github.io/api/#jsx
+
+JSXImportSource {{< new-in 0.124.0 >}}
+: (`string`) Which library to use to automatically import its JSX helper functions from. This only works if `JSX` is set to `automatic`. The specified library needs to be installed through NPM and expose certain exports. See https://esbuild.github.io/api/#jsx-import-source
+
+The combination of `JSX` and `JSXImportSource` is helpful if you want to use a non-React JSX library like Preact, e.g.:
+
+```go-html-template
+{{ $js := resources.Get "js/main.jsx" | js.Build (dict "JSX" "automatic" "JSXImportSource" "preact") }}
+```
+
+With the above, you can use Preact components and JSX without having to manually import `h` and `Fragment` every time:
+
+```jsx
+import { render } from 'preact';
+
+const App = () => <>Hello world!</>;
+
+const container = document.getElementById('app');
+if (container) render(<App />, container);
+```
+
 ### Import JS code from /assets
 
 `js.Build` has full support for the virtual union file system in [Hugo Modules](/hugo-modules/). You can see some simple examples in this [test project](https://github.com/gohugoio/hugoTestProjectJSModImports), but in short this means that you can do this:
diff --git a/docs/content/en/hugo-pipes/js.md b/docs/content/en/hugo-pipes/js.md
index 9d2bbbd82af..8a2e2f40e75 100644
--- a/docs/content/en/hugo-pipes/js.md
+++ b/docs/content/en/hugo-pipes/js.md
@@ -95,6 +95,29 @@ format
 sourceMap
 : (`string`) Whether to generate `inline` or `external` source maps from esbuild. External source maps will be written to the target with the output file name + ".map". Input source maps can be read from js.Build and node modules and combined into the output source maps. By default, source maps are not created.
 
+JSX {{< new-in 0.124.0 >}}
+: (`string`) How to handle/transform JSX syntax. One of: `transform`, `preserve`, `automatic`. Default is `transform`. Notably, the `automatic` transform was introduced in React 17+ and will cause the necessary JSX helper functions to be imported automatically. See https://esbuild.github.io/api/#jsx
+
+JSXImportSource {{< new-in 0.124.0 >}}
+: (`string`) Which library to use to automatically import its JSX helper functions from. This only works if `JSX` is set to `automatic`. The specified library needs to be installed through NPM and expose certain exports. See https://esbuild.github.io/api/#jsx-import-source
+
+The combination of `JSX` and `JSXImportSource` is helpful if you want to use a non-React JSX library like Preact, e.g.:
+
+```go-html-template
+{{ $js := resources.Get "js/main.jsx" | js.Build (dict "JSX" "automatic" "JSXImportSource" "preact") }}
+```
+
+With the above, you can use Preact components and JSX without having to manually import `h` and `Fragment` every time:
+
+```jsx
+import { render } from 'preact';
+
+const App = () => <>Hello world!</>;
+
+const container = document.getElementById('app');
+if (container) render(<App />, container);
+```
+
 ### Import JS code from /assets
 
 `js.Build` has full support for the virtual union file system in [Hugo Modules](/hugo-modules/). You can see some simple examples in this [test project](https://github.com/gohugoio/hugoTestProjectJSModImports), but in short this means that you can do this:
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
index 1c29ad67cf9..7de88638c16 100644
--- a/resources/resource_transformers/js/options.go
+++ b/resources/resource_transformers/js/options.go
@@ -86,6 +86,14 @@ type Options struct {
 	// What to use instead of React.Fragment.
 	JSXFragment string
 
+	// What to do about JSX syntax.
+	// See https://esbuild.github.io/api/#jsx
+	JSX string
+
+	// Which library to use to automatically import JSX helper functions from. Only works if JSX is set to automatic.
+	// See https://esbuild.github.io/api/#jsx-import-source
+	JSXImportSource string
+
 	// There is/was a bug in WebKit with severe performance issue with the tracking
 	// of TDZ checks in JavaScriptCore.
 	//
@@ -375,6 +383,19 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
 		return
 	}
 
+	var jsx api.JSX
+	switch opts.JSX {
+	case "", "transform":
+		jsx = api.JSXTransform
+	case "preserve":
+		jsx = api.JSXPreserve
+	case "automatic":
+		jsx = api.JSXAutomatic
+	default:
+		err = fmt.Errorf("unsupported jsx type: %q", opts.JSX)
+		return
+	}
+
 	var defines map[string]string
 	if opts.Defines != nil {
 		defines = maps.ToStringMapString(opts.Defines)
@@ -416,6 +437,9 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
 		JSXFactory:  opts.JSXFactory,
 		JSXFragment: opts.JSXFragment,
 
+		JSX:             jsx,
+		JSXImportSource: opts.JSXImportSource,
+
 		Tsconfig: opts.tsConfig,
 
 		// Note: We're not passing Sourcefile to ESBuild.


Test patch:
diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go
index b8b031b81e2..a49b174ae06 100644
--- a/resources/resource_transformers/js/options_test.go
+++ b/resources/resource_transformers/js/options_test.go
@@ -135,6 +135,20 @@ func TestToBuildOptions(t *testing.T) {
 			Loader: api.LoaderJS,
 		},
 	})
+
+	opts, err = toBuildOptions(Options{mediaType: media.Builtin.JavascriptType,
+		JSX: "automatic", JSXImportSource: "preact"})
+	c.Assert(err, qt.IsNil)
+	c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+		Bundle: true,
+		Target: api.ESNext,
+		Format: api.FormatIIFE,
+		Stdin: &api.StdinOptions{
+			Loader: api.LoaderJS,
+		},
+		JSX:             api.JSXAutomatic,
+		JSXImportSource: "preact",
+	})
 }
 
 func TestResolveComponentInAssets(t *testing.T) {