aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-12-16 08:34:17 +0100
committerBjørn Erik Pedersen <[email protected]>2024-12-16 11:52:18 +0100
commit565c30eac9e00b2ebcbdbb8e05b5e8238a15fefb (patch)
treed7a8e84098dbca1dce0d52ab0941178949532a19 /internal
parent48dd6a918a0ef070819ef80d59b2553bc99e2964 (diff)
downloadhugo-565c30eac9e00b2ebcbdbb8e05b5e8238a15fefb.tar.gz
hugo-565c30eac9e00b2ebcbdbb8e05b5e8238a15fefb.zip
js: Fix js.Batch for multihost setups
Note that this is an unreleased feature. Fixes #13151
Diffstat (limited to 'internal')
-rw-r--r--internal/js/api.go51
-rw-r--r--internal/js/esbuild/batch.go105
-rw-r--r--internal/js/esbuild/batch_integration_test.go63
3 files changed, 170 insertions, 49 deletions
diff --git a/internal/js/api.go b/internal/js/api.go
new file mode 100644
index 000000000..30180dece
--- /dev/null
+++ b/internal/js/api.go
@@ -0,0 +1,51 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+ "context"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// BatcherClient is used to do JS batch operations.
+type BatcherClient interface {
+ New(id string) (Batcher, error)
+ Store() *maps.Cache[string, Batcher]
+}
+
+// BatchPackage holds a group of JavaScript resources.
+type BatchPackage interface {
+ Groups() map[string]resource.Resources
+}
+
+// Batcher is used to build JavaScript packages.
+type Batcher interface {
+ Build(context.Context) (BatchPackage, error)
+ Config(ctx context.Context) OptionsSetter
+ Group(ctx context.Context, id string) BatcherGroup
+}
+
+// BatcherGroup is a group of scripts and instances.
+type BatcherGroup interface {
+ Instance(sid, iid string) OptionsSetter
+ Runner(id string) OptionsSetter
+ Script(id string) OptionsSetter
+}
+
+// OptionsSetter is used to set options for a batch, script or instance.
+type OptionsSetter interface {
+ SetOptions(map[string]any) string
+}
diff --git a/internal/js/esbuild/batch.go b/internal/js/esbuild/batch.go
index d0b6dba33..c5394ac0a 100644
--- a/internal/js/esbuild/batch.go
+++ b/internal/js/esbuild/batch.go
@@ -20,6 +20,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
+ "io"
"path"
"path/filepath"
"reflect"
@@ -34,7 +35,9 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/internal/js"
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources"
@@ -42,11 +45,10 @@ import (
"github.com/gohugoio/hugo/resources/resource_factories/create"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
- "github.com/spf13/afero"
"github.com/spf13/cast"
)
-var _ Batcher = (*batcher)(nil)
+var _ js.Batcher = (*batcher)(nil)
const (
NsBatch = "_hugo-js-batch"
@@ -58,7 +60,7 @@ const (
//go:embed batch-esm-runner.gotmpl
var runnerTemplateStr string
-var _ BatchPackage = (*Package)(nil)
+var _ js.BatchPackage = (*Package)(nil)
var _ buildToucher = (*optsHolder[scriptOptions])(nil)
@@ -67,16 +69,17 @@ var (
_ isBuiltOrTouchedProvider = (*scriptGroup)(nil)
)
-func NewBatcherClient(deps *deps.Deps) (*BatcherClient, error) {
+func NewBatcherClient(deps *deps.Deps) (js.BatcherClient, error) {
c := &BatcherClient{
d: deps,
buildClient: NewBuildClient(deps.BaseFs.Assets, deps.ResourceSpec),
createClient: create.New(deps.ResourceSpec),
- bundlesCache: maps.NewCache[string, BatchPackage](),
+ batcherStore: maps.NewCache[string, js.Batcher](),
+ bundlesStore: maps.NewCache[string, js.BatchPackage](),
}
deps.BuildEndListeners.Add(func(...any) bool {
- c.bundlesCache.Reset()
+ c.bundlesStore.Reset()
return false
})
@@ -125,7 +128,7 @@ func (o *opts[K, C]) Reset() {
o.h.resetCounter++
}
-func (o *opts[K, C]) Get(id uint32) OptionsSetter {
+func (o *opts[K, C]) Get(id uint32) js.OptionsSetter {
var b *optsHolder[C]
o.once.Do(func() {
b = o.h
@@ -184,18 +187,6 @@ func newOpts[K any, C optionsCompiler[C]](key K, optionsID string, defaults defa
}
}
-// BatchPackage holds a group of JavaScript resources.
-type BatchPackage interface {
- Groups() map[string]resource.Resources
-}
-
-// Batcher is used to build JavaScript packages.
-type Batcher interface {
- Build(context.Context) (BatchPackage, error)
- Config(ctx context.Context) OptionsSetter
- Group(ctx context.Context, id string) BatcherGroup
-}
-
// BatcherClient is a client for building JavaScript packages.
type BatcherClient struct {
d *deps.Deps
@@ -206,12 +197,13 @@ type BatcherClient struct {
createClient *create.Client
buildClient *BuildClient
- bundlesCache *maps.Cache[string, BatchPackage]
+ batcherStore *maps.Cache[string, js.Batcher]
+ bundlesStore *maps.Cache[string, js.BatchPackage]
}
// New creates a new Batcher with the given ID.
// This will be typically created once and reused across rebuilds.
-func (c *BatcherClient) New(id string) (Batcher, error) {
+func (c *BatcherClient) New(id string) (js.Batcher, error) {
var initErr error
c.once.Do(func() {
// We should fix the initialization order here (or use the Go template package directly), but we need to wait
@@ -288,6 +280,10 @@ func (c *BatcherClient) New(id string) (Batcher, error) {
return b, nil
}
+func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] {
+ return c.batcherStore
+}
+
func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
var buf bytes.Buffer
@@ -304,18 +300,6 @@ func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTempla
return r, s, nil
}
-// BatcherGroup is a group of scripts and instances.
-type BatcherGroup interface {
- Instance(sid, iid string) OptionsSetter
- Runner(id string) OptionsSetter
- Script(id string) OptionsSetter
-}
-
-// OptionsSetter is used to set options for a batch, script or instance.
-type OptionsSetter interface {
- SetOptions(map[string]any) string
-}
-
// Package holds a group of JavaScript resources.
type Package struct {
id string
@@ -353,9 +337,9 @@ type batcher struct {
}
// Build builds the batch if not already built or if it's stale.
-func (b *batcher) Build(ctx context.Context) (BatchPackage, error) {
+func (b *batcher) Build(ctx context.Context) (js.BatchPackage, error) {
key := dynacache.CleanKey(b.id + ".js")
- p, err := b.client.bundlesCache.GetOrCreate(key, func() (BatchPackage, error) {
+ p, err := b.client.bundlesStore.GetOrCreate(key, func() (js.BatchPackage, error) {
return b.build(ctx)
})
if err != nil {
@@ -364,11 +348,11 @@ func (b *batcher) Build(ctx context.Context) (BatchPackage, error) {
return p, nil
}
-func (b *batcher) Config(ctx context.Context) OptionsSetter {
+func (b *batcher) Config(ctx context.Context) js.OptionsSetter {
return b.configOptions.Get(b.buildCount)
}
-func (b *batcher) Group(ctx context.Context, id string) BatcherGroup {
+func (b *batcher) Group(ctx context.Context, id string) js.BatcherGroup {
if err := ValidateBatchID(id, false); err != nil {
panic(err)
}
@@ -419,7 +403,7 @@ func (b *batcher) isStale() bool {
return false
}
-func (b *batcher) build(ctx context.Context) (BatchPackage, error) {
+func (b *batcher) build(ctx context.Context) (js.BatchPackage, error) {
b.mu.Lock()
defer b.mu.Unlock()
defer func() {
@@ -463,6 +447,8 @@ func (b *batcher) doBuild(ctx context.Context) (*Package, error) {
pathGroup: maps.NewCache[string, string](),
}
+ multihostBasePaths := b.client.d.ResourceSpec.MultihostTargetBasePaths
+
// Entry points passed to ESBuid.
var entryPoints []string
addResource := func(group, pth string, r resource.Resource, isResult bool) {
@@ -701,15 +687,36 @@ func (b *batcher) doBuild(ctx context.Context) (*Package, error) {
if !handled {
// Copy to destination.
- p := strings.TrimPrefix(o.Path, outDir)
- targetFilename := filepath.Join(b.id, p)
- fs := b.client.d.BaseFs.PublishFs
- if err := fs.MkdirAll(filepath.Dir(targetFilename), 0o777); err != nil {
- return nil, fmt.Errorf("failed to create dir %q: %w", targetFilename, err)
+ // In a multihost setup, we will have multiple targets.
+ var targetFilenames []string
+ if len(multihostBasePaths) > 0 {
+ for _, base := range multihostBasePaths {
+ p := strings.TrimPrefix(o.Path, outDir)
+ targetFilename := filepath.Join(base, b.id, p)
+ targetFilenames = append(targetFilenames, targetFilename)
+ }
+ } else {
+ p := strings.TrimPrefix(o.Path, outDir)
+ targetFilename := filepath.Join(b.id, p)
+ targetFilenames = append(targetFilenames, targetFilename)
}
- if err := afero.WriteFile(fs, targetFilename, o.Contents, 0o666); err != nil {
- return nil, fmt.Errorf("failed to write to %q: %w", targetFilename, err)
+ fs := b.client.d.BaseFs.PublishFs
+
+ if err := func() error {
+ fw, err := helpers.OpenFilesForWriting(fs, targetFilenames...)
+ if err != nil {
+ return err
+ }
+ defer fw.Close()
+
+ fr := bytes.NewReader(o.Contents)
+
+ _, err = io.Copy(fw, fr)
+
+ return err
+ }(); err != nil {
+ return nil, fmt.Errorf("failed to copy to %q: %w", targetFilenames, err)
}
}
}
@@ -845,7 +852,7 @@ type optionsGetSetter[K, C any] interface {
Key() K
Reset()
- Get(uint32) OptionsSetter
+ Get(uint32) js.OptionsSetter
isStale() bool
currPrev() (map[string]any, map[string]any)
}
@@ -975,7 +982,7 @@ func (b *scriptGroup) IdentifierBase() string {
return b.id
}
-func (s *scriptGroup) Instance(sid, id string) OptionsSetter {
+func (s *scriptGroup) Instance(sid, id string) js.OptionsSetter {
if err := ValidateBatchID(sid, false); err != nil {
panic(err)
}
@@ -1014,7 +1021,7 @@ func (g *scriptGroup) Reset() {
}
}
-func (s *scriptGroup) Runner(id string) OptionsSetter {
+func (s *scriptGroup) Runner(id string) js.OptionsSetter {
if err := ValidateBatchID(id, false); err != nil {
panic(err)
}
@@ -1043,7 +1050,7 @@ func (s *scriptGroup) Runner(id string) OptionsSetter {
return s.runnersOptions[sid].Get(s.b.buildCount)
}
-func (s *scriptGroup) Script(id string) OptionsSetter {
+func (s *scriptGroup) Script(id string) js.OptionsSetter {
if err := ValidateBatchID(id, false); err != nil {
panic(err)
}
diff --git a/internal/js/esbuild/batch_integration_test.go b/internal/js/esbuild/batch_integration_test.go
index 3501f820a..55528bdf0 100644
--- a/internal/js/esbuild/batch_integration_test.go
+++ b/internal/js/esbuild/batch_integration_test.go
@@ -184,6 +184,69 @@ func TestBatchEditScriptParam(t *testing.T) {
b.AssertFileContent("public/mybatch/mygroup.js", "param-p1-main-edited")
}
+func TestBatchMultiHost(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "section"]
+[languages]
+[languages.en]
+weight = 1
+baseURL = "https://example.com/en"
+[languages.fr]
+weight = 2
+baseURL = "https://example.com/fr"
+disableLiveReload = true
+-- assets/js/styles.css --
+body {
+ background-color: red;
+}
+-- assets/js/main.js --
+import * as foo from 'mylib';
+console.log("Hello, Main!");
+-- assets/js/runner.js --
+console.log("Hello, Runner!");
+-- node_modules/mylib/index.js --
+console.log("Hello, My Lib!");
+-- layouts/index.html --
+Home.
+{{ $batch := (js.Batch "mybatch") }}
+ {{ with $batch.Config }}
+ {{ .SetOptions (dict
+ "params" (dict "id" "config")
+ "sourceMap" ""
+ )
+ }}
+{{ end }}
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer:
+{{ $batch := (js.Batch "mybatch") }}
+{{ range $k, $v := $batch.Build.Groups }}
+ {{ range $kk, $vv := . -}}
+ {{ $k }}: {{ .RelPermalink }}
+ {{ end }}
+{{ end -}}
+{{ end }}
+{{ $batch := (js.Batch "mybatch") }}
+{{ with $batch.Group "mygroup" }}
+ {{ with .Runner "run" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }}
+ {{ end }}
+ {{ with .Script "main" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }}
+ {{ end }}
+ {{ with .Instance "main" "i1" }}
+ {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }}
+ {{ end }}
+{{ end }}
+
+
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWithOSFs())
+ b.AssertPublishDir(
+ "en/mybatch/chunk-TOZKWCDE.js", "en/mybatch/mygroup.js ",
+ "fr/mybatch/mygroup.js", "fr/mybatch/chunk-TOZKWCDE.js")
+}
+
func TestBatchRenameBundledScript(t *testing.T) {
files := jsBatchFilesTemplate
b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs())