For most JavaScript building, js.Build is the right choice. The new js.Batch
function added in Hugo v0.140.0
can be used for more advanced use cases. This function allows coordinated creation of a JavaScript bundle from multiple sources (e.g. shortcodes, render hooks, page templates etc.).
Some key features:
- You can partition your scripts into groups (e.g. one group per section).
- A script can have multiple instances (e.g. multiple instances of the same React components with different options).
- A group can have one or more runners that will receive a data structure with all instances for that group with a binding of the JavaScript import of the defined
export
. - You can control how imports gets resolved in importContext. This allows you to build scripts inside page bundles with relative imports resolved in the bundle. Combine it with mount for even more control.
- This enables code splitting with sharing of common code and dependencies (e.g. React)
js.Batch
Args: ID
The Batch ID
is used to create the base directory for this batch. Forward slashes are allowed. The outline for creating a batch with one script group:
Build
The Build
method returns
As a consequence of the concurrent building, the building and inclusion of any output will need to be done in a templates.Defer
block. The example below shows the common use case of including one group’s resources in a 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" }}
<link href="{{ $s.RelPermalink }}" rel="stylesheet" />
{{ else }}
<script src="{{ $s.RelPermalink }}" type="module"></script>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
Config
Returns a OptionsSetter that can be used to set config options for the batch.
See js.Build options, but note that:
targetPath
is set automatically (there may be multiple outputs).format
must beesm
, currently the only format supporting code splitting.params
will be available in th@params/config
namespace in the scripts. This way you can import both the script or runner params and the config params with:
import * as params from "@params";
import * as config from "@params/config";
{{ 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 }}
Group
Args: ID
. No slashes.
Most of the building blocks can be seen in hdx shortcode in this project:
{{ $path := .Get "r" }}
{{ $class := .Get "c" | default "" }}
{{ $r := or (.Page.Resources.Get $path) (resources.Get $path) }}
{{ if not $r }}
{{ errorf "resource not found: %s" $path }}
{{ end }}
{{ $batch := (js.Batch "js/mybatch") }}
{{ $scriptID := $path | anchorize }}
{{ $instanceID := .Ordinal | string }}
{{ $group := .Page.RelPermalink | anchorize }}
{{ $props := .Params | default dict }}
{{ $export := .Get "export" | default "default" }}
{{ $innerData := .Inner | transform.Unmarshal | default dict }}
{{ with $innerData }}
{{ $props = $props | merge .props }}
{{ end }}
{{ with $batch.Group $group }}
{{ with .Runner "create-elements" }}
{{ .SetOptions (dict "resource" (resources.Get "js/batch/react-create-elements.js")) }}
{{ end }}
{{ with .Script $scriptID }}
{{ $common := resources.Match "/js/headlessui/*.*" }}
{{ $importContext := (slice $.Page ($common.Mount "/js/headlessui" ".")) }}
{{ .SetOptions (dict
"resource" $r
"export" $export
"importContext" $importContext )
}}
{{ end }}
{{ with .Instance $scriptID $instanceID }}
{{ .SetOptions (dict "params" $props) }}
{{ end }}
{{ end }}
<div
id="{{ printf `%s-%s` $scriptID $instanceID }}"
class="mt-6 mb-8 p-3 overflow-auto rounded-lg bg-linear-45 from-purple-900 via-blue-900 to-purple-800 {{ $class }} mystyles"></div>
Script
Args: ID
. No slashes.
Returns a OptionsSetter that can be used to set script options for this script.
{{ with js.Batch "js/mybatch" }}
{{ with .Group "mygroup" }}
{{ with .Script "myscript" }}
{{ .SetOptions (dict "resource" (resources.Get "myscript.js")) }}
{{ end }}
{{ end }}
{{ end }}
Instance
Args: SCRIPT_ID
, INSTANCE_ID
. No slashes.
Returns a OptionsSetter that can be used to set params options for this instance.
{{ with js.Batch "js/mybatch" }}
{{ with .Group "mygroup" }}
{{ with .Instance "myscript" "myinstance" }}
{{ .SetOptions (dict "params" (dict "param1" "value1")) }}
{{ end }}
{{ end }}
{{ end }}
Runner
Returns a OptionsSetter that can be used to set script options for this runner.
{{ with js.Batch "js/mybatch" }}
{{ with .Group "mygroup" }}
{{ with .Runner "myrunner" }}
{{ .SetOptions (dict "resource" (resources.Get "myrunner.js")) }}
{{ end }}
{{ end }}
{{ end }}
The runner script used in this project:
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);
//console.log('create', elId, 'with', instance.params);
const reactEl = React.createElement(script.binding, instance.params);
root.render(reactEl);
}
}
}
SetOptions
takes a map of options.
Mount
Resources.Mount
. See Buttons for more details, but in short the common use case would be to mount component folders inside /assets
to be on the same level as the Page
bundle, so you can do:
import { ButtonBasic } from "./button.jsx";
Instead of:
import { ButtonBasic } from "/js/headlessui/button.jsx";
It’s not in the example, but I guess this could be more visibly useful in situations where you need to override certain files (e.g. CSS) in the component folder.
OptionsSetter
An OptionsSetter
is a special object that is returned once only. This means that you should wrap it with with:
{{ with .Script "myscript" }}
{{ .SetOptions (dict "resource" (resources.Get "myscript.js"))}}
{{ end }}
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 isdefault
for runner scripts and*
for other scripts. - importContext
- An additional context for resolving imports. Hugo will always check this one first before falling back to
assets
andnode_modules
. A common use of this is to resolve imports inside a page bundle. - params
- A map of parameters that will be passed to the script as JSON. These gets bound to the
@params
namespace:
import * as params from '@params';
Import Context
Params Options
- params
- A map of parameters that will be passed to the script as JSON.
type Batcher interface {
Build(context.Context) (BatchPackage, error)
Config(ctx context.Context) OptionsSetter
Group(ctx context.Context, id string) BatcherGroup
}
type BatcherGroup interface {
Script(id string) OptionsSetter
Instance(sid, iid string) OptionsSetter
Runner(id string) OptionsSetter
}
type OptionsSetter interface {
SetOptions(map[string]any) string
}