Repository: gohugoio/hugo (language: go)

Issue:
[esbuild] expose the `platform` option to bundle code run on node
Dear Hugo team, I'd like request a feature to specify the `platform` option of esbuild.

**Backgrounds**

There are many static site hosting platforms that support serverless functions (e.g. Cloudlfare Worker), it would be great if we could bundle JavaScript code that runs on node, which provides better integration capabilities.

With the esbuild `platform` option, Hugo can be a Node application packing tool.

**Pros**

1. It's easy to manage, share, and reuse codes between sites through Hugo Modules.
2. Manage configuration in the same place (hugo.yaml and the `params` option of `js.Build`).

I am willing to implement it (I have a working local branch) if this request is accepted.


Discussion / hints:
Can you elaborate a little about how this ends up in e.g. a Cloudflare worker?

Fix patch:
diff --git a/internal/js/esbuild/options.go b/internal/js/esbuild/options.go
index 16fc0d4bb76..21509bc1503 100644
--- a/internal/js/esbuild/options.go
+++ b/internal/js/esbuild/options.go
@@ -121,6 +121,11 @@ type ExternalOptions struct {
 	// Default is to esm.
 	Format string
 
+	// One of browser, node, neutral.
+	// Default is browser.
+	// See https://esbuild.github.io/api/#platform
+	Platform string
+
 	// External dependencies, e.g. "react".
 	Externals []string
 
@@ -274,6 +279,19 @@ func (opts *Options) compile() (err error) {
 		return
 	}
 
+	var platform api.Platform
+	switch opts.Platform {
+	case "", "browser":
+		platform = api.PlatformBrowser
+	case "node":
+		platform = api.PlatformNode
+	case "neutral":
+		platform = api.PlatformNeutral
+	default:
+		err = fmt.Errorf("unsupported platform type: %q", opts.Platform)
+		return
+	}
+
 	var defines map[string]string
 	if opts.Defines != nil {
 		defines = maps.ToStringMapString(opts.Defines)
@@ -310,6 +328,7 @@ func (opts *Options) compile() (err error) {
 
 		Target:         target,
 		Format:         format,
+		Platform:       platform,
 		Sourcemap:      sourceMap,
 		SourcesContent: sourcesContent,
 


Test patch:
diff --git a/internal/js/esbuild/options_test.go b/internal/js/esbuild/options_test.go
index ca19717f772..7312eea2770 100644
--- a/internal/js/esbuild/options_test.go
+++ b/internal/js/esbuild/options_test.go
@@ -38,6 +38,7 @@ func TestToBuildOptions(t *testing.T) {
 		Bundle:         true,
 		Target:         api.ESNext,
 		Format:         api.FormatIIFE,
+		Platform:       api.PlatformBrowser,
 		SourcesContent: 1,
 		Stdin: &api.StdinOptions{
 			Loader: api.LoaderJS,
@@ -62,6 +63,7 @@ func TestToBuildOptions(t *testing.T) {
 		Bundle:            true,
 		Target:            api.ES2018,
 		Format:            api.FormatCommonJS,
+		Platform:          api.PlatformBrowser,
 		SourcesContent:    1,
 		MinifyIdentifiers: true,
 		MinifySyntax:      true,
@@ -87,6 +89,7 @@ func TestToBuildOptions(t *testing.T) {
 		Bundle:            true,
 		Target:            api.ES2018,
 		Format:            api.FormatCommonJS,
+		Platform:          api.PlatformBrowser,
 		MinifyIdentifiers: true,
 		MinifySyntax:      true,
 		MinifyWhitespace:  true,
@@ -113,6 +116,7 @@ func TestToBuildOptions(t *testing.T) {
 		Bundle:            true,
 		Target:            api.ES2018,
 		Format:            api.FormatCommonJS,
+		Platform:          api.PlatformBrowser,
 		MinifyIdentifiers: true,
 		MinifySyntax:      true,
 		MinifyWhitespace:  true,
@@ -139,6 +143,7 @@ func TestToBuildOptions(t *testing.T) {
 		Bundle:            true,
 		Target:            api.ES2018,
 		Format:            api.FormatCommonJS,
+		Platform:          api.PlatformBrowser,
 		MinifyIdentifiers: true,
 		MinifySyntax:      true,
 		MinifyWhitespace:  true,
@@ -164,6 +169,7 @@ func TestToBuildOptions(t *testing.T) {
 		Bundle:         true,
 		Target:         api.ESNext,
 		Format:         api.FormatIIFE,
+		Platform:       api.PlatformBrowser,
 		SourcesContent: 1,
 		Stdin: &api.StdinOptions{
 			Loader: api.LoaderJS,
@@ -210,10 +216,25 @@ func TestToBuildOptionsTarget(t *testing.T) {
 
 func TestDecodeExternalOptions(t *testing.T) {
 	c := qt.New(t)
-	m := map[string]any{}
-	opts, err := DecodeExternalOptions(m)
+	m := map[string]any{
+		"platform": "node",
+	}
+	ext, err := DecodeExternalOptions(m)
 	c.Assert(err, qt.IsNil)
-	c.Assert(opts, qt.DeepEquals, ExternalOptions{
+	c.Assert(ext, qt.DeepEquals, ExternalOptions{
 		SourcesContent: true,
+		Platform:       "node",
+	})
+
+	opts := Options{
+		ExternalOptions: ext,
+	}
+	c.Assert(opts.compile(), qt.IsNil)
+	c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+		Bundle:         true,
+		Target:         api.ESNext,
+		Format:         api.FormatIIFE,
+		Platform:       api.PlatformNode,
+		SourcesContent: api.SourcesContentInclude,
 	})
 }