--- title: js.Batch description: Build JavaScript bundle groups with global code splitting and flexible hooks/runners setup. categories: [] keywords: [] params: functions_and_methods: aliases: [] returnType: js.Batcher signatures: ['js.Batch [ID]'] --- > [!NOTE] > The `js.Batch` function is backed by the [`evanw/esbuild`][] package, providing a mature, high-performance foundation for bundling, transformation, and minification. > [!NOTE] > For a runnable example of this feature, see [this test and demo repo][js_batch_demo]. The Batch `ID` is used to create the base directory for this batch. Forward slashes are allowed. The `js.Batch` function returns an object with an API with this structure: - [Group](#group) - [Script](#script) - [SetOptions](#optionssetter) - [Instance](#instance) - [SetOptions](#optionssetter) - [Runner](#runner) - [SetOptions](#optionssetter) - [Config](#config) - [SetOptions](#optionssetter) ## Group The `Group` method take an `ID` (`string`) as argument. No slashes. It returns an object with these methods: ### Script The `Script` method takes an `ID` (`string`) as argument. No slashes. It returns an [OptionsSetter](#optionssetter) that can be used to set [script options](#script-options) for this script. ```go-html-template {{ with js.Batch "js/mybatch" }} {{ with .Group "mygroup" }} {{ with .Script "myscript" }} {{ .SetOptions (dict "resource" (resources.Get "myscript.js")) }} {{ end }} {{ end }} {{ end }} ``` `SetOptions` takes a [script options](#script-options) map. Note that if you want the script to be handled by a [Runner](#runner), you need to set the `export` option to match what you want to pass on to the runner (default is `*`). ### Instance The `Instance` method takes two `string` arguments `SCRIPT_ID` and `INSTANCE_ID`. No slashes. It returns an [OptionsSetter](#optionssetter) that can be used to set [params options](#params-options) for this instance. ```go-html-template {{ with js.Batch "js/mybatch" }} {{ with .Group "mygroup" }} {{ with .Instance "myscript" "myinstance" }} {{ .SetOptions (dict "params" (dict "param1" "value1")) }} {{ end }} {{ end }} {{ end }} ``` `SetOptions` takes a [params options](#params-options) map. The instance options will be passed to any [Runner](#runner) script in the same group, as JSON. ### Runner The `Runner` method takes an `ID` (`string`) as argument. No slashes. It returns an [OptionsSetter](#optionssetter) that can be used to set [script options](#script-options) for this runner. ```go-html-template {{ with js.Batch "js/mybatch" }} {{ with .Group "mygroup" }} {{ with .Runner "myrunner" }} {{ .SetOptions (dict "resource" (resources.Get "myrunner.js")) }} {{ end }} {{ end }} {{ end }} ``` `SetOptions` takes a [script options](#script-options) map. The runner will receive a data structure with all instances for that group with a live binding of the [JavaScript import][] of the defined `export`. The runner script's export must be a function that takes one argument, the group data structure. An example of a group data structure as JSON is: ```json { "id": "leaflet", "scripts": [ { "id": "mapjsx", "binding": JAVASCRIPT_BINDING, "instances": [ { "id": "0", "params": { "c": "h-64", "lat": 48.8533173846729, "lon": 2.3497416090232535, "r": "map.jsx", "title": "Cathédrale Notre-Dame de Paris", "zoom": 23 } }, { "id": "1", "params": { "c": "h-64", "lat": 59.96300872062237, "lon": 10.663529183196863, "r": "map.jsx", "title": "Holmenkollen", "zoom": 3 } } ] } ] } ``` Below is an example of a runner script that uses React to render elements. Note that the export (`default`) must match the `export` option in the [script options](#script-options) (`default` is the default value for runner scripts). Runnable versions of the examples on this page can be found in this `js.Batch` [demonstration repository][js_batch_demo]. ```js import * as ReactDOM from 'react-dom/client'; import * as React from 'react'; export default function Run(group) { console.log('Running react-create-elements.js', group); const scripts = group.scripts; for (const script of scripts) { for (const instance of script.instances) { /* This is a convention in this project. */ let elId = `${script.id}-${instance.id}`; let el = document.getElementById(elId); if (!el) { console.warn(`Element with id ${elId} not found`); continue; } const root = ReactDOM.createRoot(el); const reactEl = React.createElement(script.binding, instance.params); root.render(reactEl); } } } ``` ### Config Returns an [OptionsSetter](#optionssetter) that can be used to set [build options](#build-options) for the batch. These are mostly the same as for `js.Build`, but note that: - `targetPath` is set automatically (there may be multiple outputs). - `format` must be `esm`, currently the only format that supports [code splitting][]. - `params` will be available in the `@params/config` namespace in the scripts. This way you can import both the [Script](#script) or [Runner](#runner) params and the [Config](#config) params with: ```js import * as params from "@params"; import * as config from "@params/config"; ``` Setting the `Config` for a batch can be done from any template (including shortcode templates), but will only be set once (the first will win): ```go-html-template {{ with js.Batch "js/mybatch" }} {{ with .Config }} {{ .SetOptions (dict "target" "es2023" "format" "esm" "jsx" "automatic" "loaders" (dict ".png" "dataurl") "minify" true "params" (dict "param1" "value1") ) }} {{ end }} {{ end }} ``` ## Options ### Build options `format` : (`string`) Currently, `esbuild` only supports `esm` output for [code splitting][]. {{% include "/_common/functions/js/options.md" %}} ### Script options `resource` : The resource to build. This can be a file resource or a virtual resource. `export` : The export to bind the runner to. Set it to `*` to export the [entire namespace][]. Default is `default` for [Runner](#runner) scripts and `*` for other [scripts](#script). `importContext` : An additional context for resolving imports. Hugo will always check this one first before falling back to `assets` and `node_modules`. A common use of this is to resolve imports inside a page bundle. See [import context](#import-context). `params` : A map of parameters that will be passed to the script as JSON. These gets bound to the `@params` namespace: ```js import * as params from '@params'; ``` ### Params options `params` : A map of parameters that will be passed to the script as JSON. ### Import context Hugo will, by default, first try to resolve any import in the `assets` directory and, if not found, let `esbuild` resolve it (e.g. from `node_modules`). The `importContext` option can be used to set the first context for resolving imports. A common use of this is to resolve imports inside a [page bundle][]. ```go-html-template {{ $common := resources.Match "/js/headlessui/*.*" }} {{ $importContext := (slice $.Page ($common.Mount "/js/headlessui" ".")) }} ``` You can pass any object that implements [`Resource.Get`][]. Pass a slice to set multiple contexts. The example above uses [`Resources.Mount`][] to resolve a directory inside `assets` relative to the page bundle. ### OptionsSetter An `OptionsSetter` is a special object that is returned once only. This means that you should wrap it with [`with`][]: ```go-html-template {{ with .Script "myscript" }} {{ .SetOptions (dict "resource" (resources.Get "myscript.js"))}} {{ end }} ``` ## Build The `Build` method returns an object with the following structure: - Groups (map) - [`Resources`][] Each [`Resource`][] will be of media type `application/javascript` or `text/css`. In a template you would typically handle one group with a given `ID` (e.g., scripts for the current section). Because of the concurrent build, this needs to be done in a [`templates.Defer`][] block: > [!NOTE] > The [`templates.Defer`][] acts as a synchronisation point to handle scripts added concurrently by different templates. If you have a setup with where the batch is created in one go (in one template), you don't need it. > > See [this discussion][] for more information. ```go-html-template {{ $group := .group }} {{ with (templates.Defer (dict "key" $group "data" $group )) }} {{ with (js.Batch "js/mybatch") }} {{ with .Build }} {{ with index .Groups $ }} {{ range . }} {{ $s := . }} {{ if eq $s.MediaType.SubType "css" }} {{ else }} {{ end }} {{ end }} {{ end }} {{ end }} {{ end }} ``` ## Known Issues In the official documentation for the `esbuild` [code splitting][] feature, there's a warning note in the header. The two issues are: - `esm` is currently the only implemented output format. This means that it will not work for legacy browsers. See [caniuse][]. - There's a known import ordering issue. We have not seen the ordering issue as a problem during our [extensive testing][] of this new feature with different libraries. There are two main cases: 1. Undefined execution order of imports, see [this comment][comment-1] 1. Only one execution order of imports, see [this comment][comment-2] Many would say that both of the above are [code smells][]. The first one has a simple workaround in Hugo. Define the import order in its own script and make sure it gets passed early to `esbuild`, e.g., by putting it in a script group with a name that comes early in the alphabet. ```js import './lib2.js'; import './lib1.js'; console.log('entrypoints-workaround.js'); ``` [JavaScript import]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import [`Resource.Get`]: /methods/page/resources/#get [`Resource`]: /methods/resource/ [`Resources.Mount`]: /methods/page/resources/#mount [`Resources`]: /methods/page/resources/ [`evanw/esbuild`]: https://github.com/evanw/esbuild [`templates.Defer`]: /functions/templates/defer/ [`with`]: /functions/go-template/with/ [caniuse]: https://caniuse.com/?search=ESM [code smells]: https://en.wikipedia.org/wiki/Code_smell [code splitting]: https://esbuild.github.io/api/#splitting [comment-1]: https://github.com/evanw/esbuild/issues/399#issuecomment-1458680887 [comment-2]: https://github.com/evanw/esbuild/issues/399#issuecomment-735355932 [entire namespace]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#namespace_import [extensive testing]: https://github.com/bep/hugojsbatchdemo [js_batch_demo]: https://github.com/bep/hugojsbatchdemo/ [page bundle]: /content-management/page-bundles/ [this discussion]: https://discourse.gohugo.io/t/js-batch-with-simple-global-script/53002/5