js.Batch with simple global script

Hi! I’ve been trying to use js.Batch in my site, following both documentation and the example site (including its code), with no success. The example site uses scripts local to each bundle, which is not my case:

How to do code splitting in a main script (whole site) that resides in assets/js?

I have a main script (assets/js/master.js) that imports fundamental modules, and uses import() to lazyly load other modules. All those scripts also reside in assets/js. I can’t understand how I can simply write “get this main script, do the code splitting and write it to that file with all the modules files in that same directory”.

I tried so far the following 2 ways. The first, directly below, gives me error calling fingerprint: *esbuild.optsHolder[github.com/gohugoio/hugo/internal/js/esbuild.scriptOptions] can not be transformed:

{{ $group := "master.js" }}
{{ with (js.Batch "js/master.js").Config }}
  {{ .SetOptions (dict
    "format" "esm"
    "target" "es2022"
    "minify" true
    )
  }}
{{ end }}
{{ with (templates.Defer (dict "key" $group "data" $group )) }}
  {{ with (js.Batch "js/master.js") }}
    {{ with .Group "master" }}
      {{ with .Script "master" }}
        {{ .SetOptions (dict "resource" (resources.Get "js/master.js")) }}
        {{ with . | fingerprint }}
          <script
            src="{{ .RelPermalink }}"
            type="module"
            integrity="{{ .Data.Integrity }}"
            crossorigin="anonymous"></script>
        {{ end }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

Second way, which gives me no error, but also no script at all:

{{ $group := "master.js" }}
{{ with (js.Batch "js/master.js").Config }}
  {{ .SetOptions (dict
    "format" "esm"
    "target" "es2022"
    "minify" true
    )
  }}
{{ end }}
{{ with (templates.Defer (dict "key" $group "data" $group )) }}
  {{ with (js.Batch "js/master.js") }}
    {{ with .Build }}
      {{ with index .Groups $ }}
        {{ range . }}
          {{ with . | fingerprint }}
            <script
              src="{{ .RelPermalink }}"
              type="module"
              integrity="{{ .Data.Integrity }}"
              crossorigin="anonymous"></script>
          {{ end }}
        {{ end }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

I have just skimmed your code, but my best guess is this, and the order is important:

  1. You need to build your scripts outside of any defer, e.g. " {{ with .Script “master” }}"
  2. The only thing inside defer would be calling .Build and including the scripts.

If you look at the demo repo, that is the pattern used.

It works! For anyone trying to understand this, I think the general idea is:

There are two “moments” in js.Batch: one for configuration, including which resources (e.g. script files) will be transformed, and another where the resources are actually transformed. The first moment occurs outside any templates.Defer (as @bep pointed out above), with .Config, .Script etc, setting which groups will be transformed (even if it’s only one). The second moment occurs inside templates.Defer, with .Build and the .Groups being treated and transformed in actual files (like <script>, <link> etc). Those two moments can occur in different files along your site.

Below, for future reference, the code I’m using (I’ll still improve the configuration for my use case). I’ve separated the two “moments” with comments, but in my site they are contiguous:

{{/* 1st moment */}}
{{ $group := "master" }}
{{ with (js.Batch "js/master.js") }}
  {{ with .Config }}
    {{ .SetOptions (dict
      "format" "esm"
      "target" "es2022"
      "minify" true
      )
    }}
  {{ end }}
  {{ with .Group $group }}
    {{ with .Script "master" }}
      {{ .SetOptions (dict "resource" (resources.Get "js/master.js")) }}
    {{ end }}
  {{ end }}
{{ end }}

{{/* 2nd moment */}}
{{ with (templates.Defer (dict "key" $group "data" $group )) }}
  {{ with (js.Batch "js/master.js") }}
    {{ with .Build }}
      {{ with index .Groups $ }}
        {{ range . }}
          {{ with . | fingerprint }}
            <script
              src="{{ .RelPermalink }}"
              type="module"
              integrity="{{ .Data.Integrity }}"
              crossorigin="anonymous"></script>
          {{ end }}
        {{ end }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

Note that if they’re contiguous, you can drop the Defer. The Defer effectively acts as a synchronisation point to Build JS entry points added to the batch during the build (from many different templates).

If you’re only adding 1 script into 1 group, then most of the benefits of this js.Batch function goes away (that is: code splitting). You do get CSS support and the “runner” thing, though, so if that’s the case, then it makes sense.

1 Like

Thank you, @bep , it really works without the Defer!

Maybe I’ve poorly explained my case: I have one entry point module, which itself imports many others, most of them dynamically (via import()). Before js.Batch, all those modules ended up in one single, giant file. Now they are correctly split and only get loaded when needed. So, I do benefit from code splitting.

I couldn’t understand the reasoning of the CSS support, nor the “runner” thing, but they seem to relate to frameworks (React and the like), am I wrong? I don’t use any framework, just TailwindCSS (even without their UI components).

CSS: You can import CSS files into your JS files, and a CSS bundle (per group) will be created. Whether that is useful or not, I’m not sure, but it can certainly be for more complex setups.

Runner: The React use case is obvious, but I’m sure there are others as it is a fairly general and simple mechanism. It’s basically a way to add a special script that gets passed a datastructure with all the exports in a group … and these (assuming they are functions) can then be invoked.

Yea, OK, I didn’t think about that. Yea, that’s obviously very cool.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.