aboutsummaryrefslogtreecommitdiffhomepage
path: root/resources
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-12-10 16:22:08 +0100
committerBjørn Erik Pedersen <[email protected]>2024-12-12 21:43:17 +0100
commite293e7ca6dcc34cded7eb90a644b5c720c2179cf (patch)
treedee8e8d272660d23aa7b76576e8267fc70c34f78 /resources
parent157d86414d43f6801e2a6996108f67d28679eac5 (diff)
downloadhugo-e293e7ca6dcc34cded7eb90a644b5c720c2179cf.tar.gz
hugo-e293e7ca6dcc34cded7eb90a644b5c720c2179cf.zip
Add js.Batch
Fixes #12626 Closes #7499 Closes #9978 Closes #12879 Closes #13113 Fixes #13116
Diffstat (limited to 'resources')
-rw-r--r--resources/image.go5
-rw-r--r--resources/page/page.go3
-rw-r--r--resources/resource.go54
-rw-r--r--resources/resource/resource_helpers.go3
-rw-r--r--resources/resource/resources.go184
-rw-r--r--resources/resource/resources_integration_test.go105
-rw-r--r--resources/resource/resources_test.go122
-rw-r--r--resources/resource_factories/create/create.go19
-rw-r--r--resources/resource_metadata.go4
-rw-r--r--resources/resource_test.go18
-rw-r--r--resources/resource_transformers/js/build.go200
-rw-r--r--resources/resource_transformers/js/build_test.go14
-rw-r--r--resources/resource_transformers/js/js_integration_test.go28
-rw-r--r--resources/resource_transformers/js/options.go461
-rw-r--r--resources/resource_transformers/js/options_test.go241
-rw-r--r--resources/resource_transformers/js/transform.go68
-rw-r--r--resources/resource_transformers/tocss/dartsass/transform.go4
-rw-r--r--resources/transform.go15
18 files changed, 638 insertions, 910 deletions
diff --git a/resources/image.go b/resources/image.go
index 4595866d4..686f70e27 100644
--- a/resources/image.go
+++ b/resources/image.go
@@ -51,6 +51,7 @@ var (
_ resource.Source = (*imageResource)(nil)
_ resource.Cloner = (*imageResource)(nil)
_ resource.NameNormalizedProvider = (*imageResource)(nil)
+ _ targetPathProvider = (*imageResource)(nil)
)
// imageResource represents an image resource.
@@ -160,6 +161,10 @@ func (i *imageResource) Colors() ([]images.Color, error) {
return i.dominantColors, nil
}
+func (i *imageResource) targetPath() string {
+ return i.TargetPath()
+}
+
// Clone is for internal use.
func (i *imageResource) Clone() resource.Resource {
gr := i.baseResource.Clone().(baseResource)
diff --git a/resources/page/page.go b/resources/page/page.go
index 032ee320d..40e15dc6f 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -63,8 +63,7 @@ type ChildCareProvider interface {
// section.
RegularPagesRecursive() Pages
- // Resources returns a list of all resources.
- Resources() resource.Resources
+ resource.ResourcesProvider
}
type MarkupProvider interface {
diff --git a/resources/resource.go b/resources/resource.go
index cc7008e5a..6025cbf4c 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -47,6 +47,8 @@ var (
_ resource.Cloner = (*genericResource)(nil)
_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
_ resource.Identifier = (*genericResource)(nil)
+ _ targetPathProvider = (*genericResource)(nil)
+ _ sourcePathProvider = (*genericResource)(nil)
_ identity.IdentityGroupProvider = (*genericResource)(nil)
_ identity.DependencyManagerProvider = (*genericResource)(nil)
_ identity.Identity = (*genericResource)(nil)
@@ -79,6 +81,7 @@ type ResourceSourceDescriptor struct {
TargetPath string
BasePathRelPermalink string
BasePathTargetPath string
+ SourceFilenameOrPath string // Used for error logging.
// The Data to associate with this resource.
Data map[string]any
@@ -463,6 +466,17 @@ func (l *genericResource) Key() string {
return key
}
+func (l *genericResource) targetPath() string {
+ return l.paths.TargetPath()
+}
+
+func (l *genericResource) sourcePath() string {
+ if p := l.sd.SourceFilenameOrPath; p != "" {
+ return p
+ }
+ return ""
+}
+
func (l *genericResource) MediaType() media.Type {
return l.sd.MediaType
}
@@ -660,3 +674,43 @@ func (r *resourceHash) init(l hugio.ReadSeekCloserProvider) error {
func hashImage(r io.ReadSeeker) (uint64, int64, error) {
return hashing.XXHashFromReader(r)
}
+
+// InternalResourceTargetPath is used internally to get the target path for a Resource.
+func InternalResourceTargetPath(r resource.Resource) string {
+ return r.(targetPathProvider).targetPath()
+}
+
+// InternalResourceSourcePathBestEffort is used internally to get the source path for a Resource.
+// It returns an empty string if the source path is not available.
+func InternalResourceSourcePath(r resource.Resource) string {
+ if sp, ok := r.(sourcePathProvider); ok {
+ if p := sp.sourcePath(); p != "" {
+ return p
+ }
+ }
+ return ""
+}
+
+// InternalResourceSourcePathBestEffort is used internally to get the source path for a Resource.
+// Used for error messages etc.
+// It will fall back to the target path if the source path is not available.
+func InternalResourceSourcePathBestEffort(r resource.Resource) string {
+ if s := InternalResourceSourcePath(r); s != "" {
+ return s
+ }
+ return InternalResourceTargetPath(r)
+}
+
+type targetPathProvider interface {
+ // targetPath is the relative path to this resource.
+ // In most cases this will be the same as the RelPermalink(),
+ // but it will not trigger any lazy publishing.
+ targetPath() string
+}
+
+// Optional interface implemented by resources that can provide the source path.
+type sourcePathProvider interface {
+ // sourcePath is the source path to this resource's source.
+ // This is used in error messages etc.
+ sourcePath() string
+}
diff --git a/resources/resource/resource_helpers.go b/resources/resource/resource_helpers.go
index c2bb463c8..8575ae79e 100644
--- a/resources/resource/resource_helpers.go
+++ b/resources/resource/resource_helpers.go
@@ -14,10 +14,11 @@
package resource
import (
- "github.com/gohugoio/hugo/common/maps"
"strings"
"time"
+ "github.com/gohugoio/hugo/common/maps"
+
"github.com/gohugoio/hugo/helpers"
"github.com/pelletier/go-toml/v2"
diff --git a/resources/resource/resources.go b/resources/resource/resources.go
index 32bcdbb08..480c703b5 100644
--- a/resources/resource/resources.go
+++ b/resources/resource/resources.go
@@ -16,8 +16,11 @@ package resource
import (
"fmt"
+ "path"
"strings"
+ "github.com/gohugoio/hugo/common/hreflect"
+ "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/spf13/cast"
@@ -29,6 +32,51 @@ var _ ResourceFinder = (*Resources)(nil)
// I.e. both pages and images etc.
type Resources []Resource
+// Mount mounts the given resources from base to the given target path.
+// Note that leading slashes in target marks an absolute path.
+// This method is currently only useful in js.Batch.
+func (r Resources) Mount(base, target string) ResourceGetter {
+ return resourceGetterFunc(func(namev any) Resource {
+ name1, err := cast.ToStringE(namev)
+ if err != nil {
+ panic(err)
+ }
+
+ isTargetAbs := strings.HasPrefix(target, "/")
+
+ if target != "" {
+ name1 = strings.TrimPrefix(name1, target)
+ if !isTargetAbs {
+ name1 = paths.TrimLeading(name1)
+ }
+ }
+
+ if base != "" && isTargetAbs {
+ name1 = path.Join(base, name1)
+ }
+
+ for _, res := range r {
+ name2 := res.Name()
+
+ if base != "" && !isTargetAbs {
+ name2 = paths.TrimLeading(strings.TrimPrefix(name2, base))
+ }
+
+ if strings.EqualFold(name1, name2) {
+ return res
+ }
+
+ }
+
+ return nil
+ })
+}
+
+type ResourcesProvider interface {
+ // Resources returns a list of all resources.
+ Resources() Resources
+}
+
// var _ resource.ResourceFinder = (*Namespace)(nil)
// ResourcesConverter converts a given slice of Resource objects to Resources.
type ResourcesConverter interface {
@@ -63,13 +111,25 @@ func (r Resources) Get(name any) Resource {
panic(err)
}
- namestr = paths.AddLeadingSlash(namestr)
+ isDotCurrent := strings.HasPrefix(namestr, "./")
+ if isDotCurrent {
+ namestr = strings.TrimPrefix(namestr, "./")
+ } else {
+ namestr = paths.AddLeadingSlash(namestr)
+ }
+
+ check := func(name string) bool {
+ if !isDotCurrent {
+ name = paths.AddLeadingSlash(name)
+ }
+ return strings.EqualFold(namestr, name)
+ }
// First check the Name.
// Note that this can be modified by the user in the front matter,
// also, it does not contain any language code.
for _, resource := range r {
- if strings.EqualFold(namestr, paths.AddLeadingSlash(resource.Name())) {
+ if check(resource.Name()) {
return resource
}
}
@@ -77,7 +137,7 @@ func (r Resources) Get(name any) Resource {
// Finally, check the normalized name.
for _, resource := range r {
if nop, ok := resource.(NameNormalizedProvider); ok {
- if strings.EqualFold(namestr, paths.AddLeadingSlash(nop.NameNormalized())) {
+ if check(nop.NameNormalized()) {
return resource
}
}
@@ -197,14 +257,35 @@ type Source interface {
Publish() error
}
-// ResourceFinder provides methods to find Resources.
-// Note that GetRemote (as found in resources.GetRemote) is
-// not covered by this interface, as this is only available as a global template function.
-type ResourceFinder interface {
+type ResourceGetter interface {
// Get locates the Resource with the given name in the current context (e.g. in .Page.Resources).
//
// It returns nil if no Resource could found, panics if name is invalid.
Get(name any) Resource
+}
+
+type IsProbablySameResourceGetter interface {
+ IsProbablySameResourceGetter(other ResourceGetter) bool
+}
+
+// StaleInfoResourceGetter is a ResourceGetter that also provides information about
+// whether the underlying resources are stale.
+type StaleInfoResourceGetter interface {
+ StaleInfo
+ ResourceGetter
+}
+
+type resourceGetterFunc func(name any) Resource
+
+func (f resourceGetterFunc) Get(name any) Resource {
+ return f(name)
+}
+
+// ResourceFinder provides methods to find Resources.
+// Note that GetRemote (as found in resources.GetRemote) is
+// not covered by this interface, as this is only available as a global template function.
+type ResourceFinder interface {
+ ResourceGetter
// GetMatch finds the first Resource matching the given pattern, or nil if none found.
//
@@ -235,3 +316,92 @@ type ResourceFinder interface {
// It returns nil if no Resources could found, panics if typ is invalid.
ByType(typ any) Resources
}
+
+// NewCachedResourceGetter creates a new ResourceGetter from the given objects.
+// If multiple objects are provided, they are merged into one where
+// the first match wins.
+func NewCachedResourceGetter(os ...any) *cachedResourceGetter {
+ var getters multiResourceGetter
+ for _, o := range os {
+ if g, ok := unwrapResourceGetter(o); ok {
+ getters = append(getters, g)
+ }
+ }
+
+ return &cachedResourceGetter{
+ cache: maps.NewCache[string, Resource](),
+ delegate: getters,
+ }
+}
+
+type multiResourceGetter []ResourceGetter
+
+func (m multiResourceGetter) Get(name any) Resource {
+ for _, g := range m {
+ if res := g.Get(name); res != nil {
+ return res
+ }
+ }
+ return nil
+}
+
+var (
+ _ ResourceGetter = (*cachedResourceGetter)(nil)
+ _ IsProbablySameResourceGetter = (*cachedResourceGetter)(nil)
+)
+
+type cachedResourceGetter struct {
+ cache *maps.Cache[string, Resource]
+ delegate ResourceGetter
+}
+
+func (c *cachedResourceGetter) Get(name any) Resource {
+ namestr, err := cast.ToStringE(name)
+ if err != nil {
+ panic(err)
+ }
+ v, _ := c.cache.GetOrCreate(namestr, func() (Resource, error) {
+ v := c.delegate.Get(name)
+ return v, nil
+ })
+ return v
+}
+
+func (c *cachedResourceGetter) IsProbablySameResourceGetter(other ResourceGetter) bool {
+ isProbablyEq := true
+ c.cache.ForEeach(func(k string, v Resource) bool {
+ if v != other.Get(k) {
+ isProbablyEq = false
+ return false
+ }
+ return true
+ })
+
+ return isProbablyEq
+}
+
+func unwrapResourceGetter(v any) (ResourceGetter, bool) {
+ if v == nil {
+ return nil, false
+ }
+ switch vv := v.(type) {
+ case ResourceGetter:
+ return vv, true
+ case ResourcesProvider:
+ return vv.Resources(), true
+ case func(name any) Resource:
+ return resourceGetterFunc(vv), true
+ default:
+ vvv, ok := hreflect.ToSliceAny(v)
+ if !ok {
+ return nil, false
+ }
+ var getters multiResourceGetter
+ for _, vv := range vvv {
+ if g, ok := unwrapResourceGetter(vv); ok {
+ getters = append(getters, g)
+ }
+ }
+ return getters, len(getters) > 0
+ }
+}
diff --git a/resources/resource/resources_integration_test.go b/resources/resource/resources_integration_test.go
new file mode 100644
index 000000000..920fc7397
--- /dev/null
+++ b/resources/resource/resources_integration_test.go
@@ -0,0 +1,105 @@
+// 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 resource_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestResourcesMount(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- assets/text/txt1.txt --
+Text 1.
+-- assets/text/txt2.txt --
+Text 2.
+-- assets/text/sub/txt3.txt --
+Text 3.
+-- assets/text/sub/txt4.txt --
+Text 4.
+-- content/mybundle/index.md --
+---
+title: "My Bundle"
+---
+-- content/mybundle/txt1.txt --
+Text 1.
+-- content/mybundle/sub/txt2.txt --
+Text 1.
+-- layouts/index.html --
+{{ $mybundle := site.GetPage "mybundle" }}
+{{ $subResources := resources.Match "/text/sub/*.*" }}
+{{ $subResourcesMount := $subResources.Mount "/text/sub" "/newroot" }}
+resources:text/txt1.txt:{{ with resources.Get "text/txt1.txt" }}{{ .Name }}{{ end }}|
+resources:text/txt2.txt:{{ with resources.Get "text/txt2.txt" }}{{ .Name }}{{ end }}|
+resources:text/sub/txt3.txt:{{ with resources.Get "text/sub/txt3.txt" }}{{ .Name }}{{ end }}|
+subResources.range:{{ range $subResources }}{{ .Name }}|{{ end }}|
+subResources:"text/sub/txt3.txt:{{ with $subResources.Get "text/sub/txt3.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount:/newroot/txt3.txt:{{ with $subResourcesMount.Get "/newroot/txt3.txt" }}{{ .Name }}{{ end }}|
+page:txt1.txt:{{ with $mybundle.Resources.Get "txt1.txt" }}{{ .Name }}{{ end }}|
+page:./txt1.txt:{{ with $mybundle.Resources.Get "./txt1.txt" }}{{ .Name }}{{ end }}|
+page:sub/txt2.txt:{{ with $mybundle.Resources.Get "sub/txt2.txt" }}{{ .Name }}{{ end }}|
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", `
+resources:text/txt1.txt:/text/txt1.txt|
+resources:text/txt2.txt:/text/txt2.txt|
+resources:text/sub/txt3.txt:/text/sub/txt3.txt|
+subResources:"text/sub/txt3.txt:/text/sub/txt3.txt|
+subResourcesMount:/newroot/txt3.txt:/text/sub/txt3.txt|
+page:txt1.txt:txt1.txt|
+page:./txt1.txt:txt1.txt|
+page:sub/txt2.txt:sub/txt2.txt|
+`)
+}
+
+func TestResourcesMountOnRename(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "home", "sitemap"]
+-- content/mybundle/index.md --
+---
+title: "My Bundle"
+resources:
+- name: /foo/bars.txt
+ src: foo/txt1.txt
+- name: foo/bars2.txt
+ src: foo/txt2.txt
+---
+-- content/mybundle/foo/txt1.txt --
+Text 1.
+-- content/mybundle/foo/txt2.txt --
+Text 2.
+-- layouts/_default/single.html --
+Single.
+{{ $mybundle := site.GetPage "mybundle" }}
+Resources:{{ range $mybundle.Resources }}Name: {{ .Name }}|{{ end }}$
+{{ $subResourcesMount := $mybundle.Resources.Mount "/foo" "/newroot" }}
+ {{ $subResourcesMount2 := $mybundle.Resources.Mount "foo" "/newroot" }}
+{{ $subResourcesMount3 := $mybundle.Resources.Mount "foo" "." }}
+subResourcesMount:/newroot/bars.txt:{{ with $subResourcesMount.Get "/newroot/bars.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount:/newroot/bars2.txt:{{ with $subResourcesMount.Get "/newroot/bars2.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount2:/newroot/bars2.txt:{{ with $subResourcesMount2.Get "/newroot/bars2.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount3:bars2.txt:{{ with $subResourcesMount3.Get "bars2.txt" }}{{ .Name }}{{ end }}|
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/mybundle/index.html",
+ "Resources:Name: foo/bars.txt|Name: foo/bars2.txt|$",
+ "subResourcesMount:/newroot/bars.txt:|\nsubResourcesMount:/newroot/bars2.txt:|",
+ "subResourcesMount2:/newroot/bars2.txt:foo/bars2.txt|",
+ "subResourcesMount3:bars2.txt:foo/bars2.txt|",
+ )
+}
diff --git a/resources/resource/resources_test.go b/resources/resource/resources_test.go
new file mode 100644
index 000000000..ebadbb312
--- /dev/null
+++ b/resources/resource/resources_test.go
@@ -0,0 +1,122 @@
+// 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 resource
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestResourcesMount(t *testing.T) {
+ c := qt.New(t)
+ c.Assert(true, qt.IsTrue)
+
+ var m ResourceGetter
+ var r Resources
+
+ check := func(in, expect string) {
+ c.Helper()
+ r := m.Get(in)
+ c.Assert(r, qt.Not(qt.IsNil))
+ c.Assert(r.Name(), qt.Equals, expect)
+ }
+
+ checkNil := func(in string) {
+ c.Helper()
+ r := m.Get(in)
+ c.Assert(r, qt.IsNil)
+ }
+
+ // Misc tests.
+ r = Resources{
+ testResource{name: "/foo/theme.css"},
+ }
+
+ m = r.Mount("/foo", ".")
+ check("./theme.css", "/foo/theme.css")
+
+ // Relative target.
+ r = Resources{
+ testResource{name: "/a/b/c/d.txt"},
+ testResource{name: "/a/b/c/e/f.txt"},
+ testResource{name: "/a/b/d.txt"},
+ testResource{name: "/a/b/e.txt"},
+ }
+
+ m = r.Mount("/a/b/c", "z")
+ check("z/d.txt", "/a/b/c/d.txt")
+ check("z/e/f.txt", "/a/b/c/e/f.txt")
+
+ m = r.Mount("/a/b", "")
+ check("d.txt", "/a/b/d.txt")
+ m = r.Mount("/a/b", ".")
+ check("d.txt", "/a/b/d.txt")
+ m = r.Mount("/a/b", "./")
+ check("d.txt", "/a/b/d.txt")
+ check("./d.txt", "/a/b/d.txt")
+
+ m = r.Mount("/a/b", ".")
+ check("./d.txt", "/a/b/d.txt")
+
+ // Absolute target.
+ m = r.Mount("/a/b/c", "/z")
+ check("/z/d.txt", "/a/b/c/d.txt")
+ check("/z/e/f.txt", "/a/b/c/e/f.txt")
+ checkNil("/z/f.txt")
+
+ m = r.Mount("/a/b", "/z")
+ check("/z/c/d.txt", "/a/b/c/d.txt")
+ check("/z/c/e/f.txt", "/a/b/c/e/f.txt")
+ check("/z/d.txt", "/a/b/d.txt")
+ checkNil("/z/f.txt")
+
+ m = r.Mount("", "")
+ check("/a/b/c/d.txt", "/a/b/c/d.txt")
+ check("/a/b/c/e/f.txt", "/a/b/c/e/f.txt")
+ check("/a/b/d.txt", "/a/b/d.txt")
+ checkNil("/a/b/f.txt")
+
+ m = r.Mount("/a/b", "/a/b")
+ check("/a/b/c/d.txt", "/a/b/c/d.txt")
+ check("/a/b/c/e/f.txt", "/a/b/c/e/f.txt")
+ check("/a/b/d.txt", "/a/b/d.txt")
+ checkNil("/a/b/f.txt")
+
+ // Resources with relative paths.
+ r = Resources{
+ testResource{name: "a/b/c/d.txt"},
+ testResource{name: "a/b/c/e/f.txt"},
+ testResource{name: "a/b/d.txt"},
+ testResource{name: "a/b/e.txt"},
+ testResource{name: "n.txt"},
+ }
+
+ m = r.Mount("a/b", "z")
+ check("z/d.txt", "a/b/d.txt")
+ checkNil("/z/d.txt")
+}
+
+type testResource struct {
+ Resource
+ name string
+}
+
+func (r testResource) Name() string {
+ return r.name
+}
+
+func (r testResource) NameNormalized() string {
+ return r.name
+}
diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go
index 2d868bd15..7dd26f4c0 100644
--- a/resources/resource_factories/create/create.go
+++ b/resources/resource_factories/create/create.go
@@ -143,16 +143,18 @@ func (c *Client) Get(pathname string) (resource.Resource, error) {
return nil, err
}
- pi := fi.(hugofs.FileMetaInfo).Meta().PathInfo
+ meta := fi.(hugofs.FileMetaInfo).Meta()
+ pi := meta.PathInfo
return c.rs.NewResource(resources.ResourceSourceDescriptor{
LazyPublish: true,
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
return c.rs.BaseFs.Assets.Fs.Open(filename)
},
- Path: pi,
- GroupIdentity: pi,
- TargetPath: pathname,
+ Path: pi,
+ GroupIdentity: pi,
+ TargetPath: pathname,
+ SourceFilenameOrPath: meta.Filename,
})
})
}
@@ -196,10 +198,11 @@ func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource)
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
return meta.Open()
},
- NameNormalized: meta.PathInfo.Path(),
- NameOriginal: meta.PathInfo.Unnormalized().Path(),
- GroupIdentity: meta.PathInfo,
- TargetPath: meta.PathInfo.Unnormalized().Path(),
+ NameNormalized: meta.PathInfo.Path(),
+ NameOriginal: meta.PathInfo.Unnormalized().Path(),
+ GroupIdentity: meta.PathInfo,
+ TargetPath: meta.PathInfo.Unnormalized().Path(),
+ SourceFilenameOrPath: meta.Filename,
})
if err != nil {
return true, err
diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go
index 7d2459225..8861ded5c 100644
--- a/resources/resource_metadata.go
+++ b/resources/resource_metadata.go
@@ -15,6 +15,7 @@ package resources
import (
"fmt"
+ "path/filepath"
"strconv"
"strings"
@@ -26,6 +27,7 @@ import (
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
)
var (
@@ -172,6 +174,8 @@ func assignMetadata(metadata []map[string]any, ma *metaResource) error {
name, found := meta["name"]
if found {
name := cast.ToString(name)
+ // Bundled resources in sub folders are relative paths with forward slashes. Make sure any renames also matches that format:
+ name = paths.TrimLeading(filepath.ToSlash(name))
if !nameCounterFound {
nameCounterFound = strings.Contains(name, counterPlaceHolder)
}
diff --git a/resources/resource_test.go b/resources/resource_test.go
index ccce8eff7..d07770456 100644
--- a/resources/resource_test.go
+++ b/resources/resource_test.go
@@ -16,8 +16,26 @@ package resources
import (
"os"
"testing"
+
+ qt "github.com/frankban/quicktest"
)
+func TestAtomicStaler(t *testing.T) {
+ c := qt.New(t)
+
+ type test struct {
+ AtomicStaler
+ }
+
+ var v test
+
+ c.Assert(v.StaleVersion(), qt.Equals, uint32(0))
+ v.MarkStale()
+ c.Assert(v.StaleVersion(), qt.Equals, uint32(1))
+ v.MarkStale()
+ c.Assert(v.StaleVersion(), qt.Equals, uint32(2))
+}
+
func BenchmarkHashImage(b *testing.B) {
f, err := os.Open("testdata/sunset.jpg")
if err != nil {
diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go
index cc68d2253..bd943461f 100644
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
+// 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.
@@ -14,209 +14,69 @@
package js
import (
- "errors"
- "fmt"
- "io"
- "os"
"path"
- "path/filepath"
"regexp"
- "strings"
-
- "github.com/spf13/afero"
-
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/media"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/common/text"
+ "github.com/evanw/esbuild/pkg/api"
"github.com/gohugoio/hugo/hugolib/filesystems"
- "github.com/gohugoio/hugo/resources/internal"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
- "github.com/evanw/esbuild/pkg/api"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
)
// Client context for ESBuild.
type Client struct {
- rs *resources.Spec
- sfs *filesystems.SourceFilesystem
+ c *esbuild.BuildClient
}
// New creates a new client context.
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
return &Client{
- rs: rs,
- sfs: fs,
+ c: esbuild.NewBuildClient(fs, rs),
}
}
-type buildTransformation struct {
- optsm map[string]any
- c *Client
-}
-
-func (t *buildTransformation) Key() internal.ResourceTransformationKey {
- return internal.NewResourceTransformationKey("jsbuild", t.optsm)
+// Process processes a resource with the user provided options.
+func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
+ return res.Transform(
+ &buildTransformation{c: c, optsm: opts},
+ )
}
-func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
- ctx.OutMediaType = media.Builtin.JavascriptType
-
- opts, err := decodeOptions(t.optsm)
- if err != nil {
- return err
+func (c *Client) transform(opts esbuild.Options, transformCtx *resources.ResourceTransformationCtx) (api.BuildResult, error) {
+ if transformCtx.DependencyManager != nil {
+ opts.DependencyManager = transformCtx.DependencyManager
}
- if opts.TargetPath != "" {
- ctx.OutPath = opts.TargetPath
- } else {
- ctx.ReplaceOutPathExtension(".js")
- }
+ opts.StdinSourcePath = transformCtx.SourcePath
- src, err := io.ReadAll(ctx.From)
+ result, err := c.c.Build(opts)
if err != nil {
- return err
+ return result, err
}
- opts.sourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
- opts.resolveDir = t.c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
- opts.contents = string(src)
- opts.mediaType = ctx.InMediaType
- opts.tsConfig = t.c.rs.ResolveJSConfigFile("tsconfig.json")
-
- buildOptions, err := toBuildOptions(opts)
- if err != nil {
- return err
- }
-
- buildOptions.Plugins, err = createBuildPlugins(ctx.DependencyManager, t.c, opts)
- if err != nil {
- return err
- }
-
- if buildOptions.Sourcemap == api.SourceMapExternal && buildOptions.Outdir == "" {
- buildOptions.Outdir, err = os.MkdirTemp(os.TempDir(), "compileOutput")
- if err != nil {
- return err
- }
- defer os.Remove(buildOptions.Outdir)
- }
-
- if opts.Inject != nil {
- // Resolve the absolute filenames.
- for i, ext := range opts.Inject {
- impPath := filepath.FromSlash(ext)
- if filepath.IsAbs(impPath) {
- return fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
- }
-
- m := resolveComponentInAssets(t.c.rs.Assets.Fs, impPath)
-
- if m == nil {
- return fmt.Errorf("inject: file %q not found", ext)
- }
-
- opts.Inject[i] = m.Filename
-
- }
-
- buildOptions.Inject = opts.Inject
-
- }
-
- result := api.Build(buildOptions)
-
- if len(result.Errors) > 0 {
-
- createErr := func(msg api.Message) error {
- loc := msg.Location
- if loc == nil {
- return errors.New(msg.Text)
- }
- path := loc.File
- if path == stdinImporter {
- path = ctx.SourcePath
- }
-
- errorMessage := msg.Text
- errorMessage = strings.ReplaceAll(errorMessage, nsImportHugo+":", "")
-
- var (
- f afero.File
- err error
- )
-
- if strings.HasPrefix(path, nsImportHugo) {
- path = strings.TrimPrefix(path, nsImportHugo+":")
- f, err = hugofs.Os.Open(path)
- } else {
- var fi os.FileInfo
- fi, err = t.c.sfs.Fs.Stat(path)
- if err == nil {
- m := fi.(hugofs.FileMetaInfo).Meta()
- path = m.Filename
- f, err = m.Open()
- }
-
- }
-
- if err == nil {
- fe := herrors.
- NewFileErrorFromName(errors.New(errorMessage), path).
- UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}).
- UpdateContent(f, nil)
-
- f.Close()
- return fe
- }
-
- return fmt.Errorf("%s", errorMessage)
- }
-
- var errors []error
-
- for _, msg := range result.Errors {
- errors = append(errors, createErr(msg))
- }
-
- // Return 1, log the rest.
- for i, err := range errors {
- if i > 0 {
- t.c.rs.Logger.Errorf("js.Build failed: %s", err)
- }
- }
-
- return errors[0]
- }
-
- if buildOptions.Sourcemap == api.SourceMapExternal {
+ if opts.ExternalOptions.SourceMap == "linked" || opts.ExternalOptions.SourceMap == "external" {
content := string(result.OutputFiles[1].Contents)
- symPath := path.Base(ctx.OutPath) + ".map"
- re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
- content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
+ if opts.ExternalOptions.SourceMap == "linked" {
+ symPath := path.Base(transformCtx.OutPath) + ".map"
+ re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
+ content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
+ }
- if err = ctx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
- return err
+ if err = transformCtx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
+ return result, err
}
- _, err := ctx.To.Write([]byte(content))
+ _, err := transformCtx.To.Write([]byte(content))
if err != nil {
- return err
+ return result, err
}
} else {
- _, err := ctx.To.Write(result.OutputFiles[0].Contents)
+ _, err := transformCtx.To.Write(result.OutputFiles[0].Contents)
if err != nil {
- return err
+ return result, err
}
- }
- return nil
-}
-// Process process esbuild transform
-func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
- return res.Transform(
- &buildTransformation{c: c, optsm: opts},
- )
+ }
+ return result, nil
}
diff --git a/resources/resource_transformers/js/build_test.go b/resources/resource_transformers/js/build_test.go
deleted file mode 100644
index 30a4490ed..000000000
--- a/resources/resource_transformers/js/build_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2020 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
diff --git a/resources/resource_transformers/js/js_integration_test.go b/resources/resource_transformers/js/js_integration_test.go
index 304c51d33..c62312ef5 100644
--- a/resources/resource_transformers/js/js_integration_test.go
+++ b/resources/resource_transformers/js/js_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Hugo Authors. All rights reserved.
+// 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.
@@ -14,13 +14,16 @@
package js_test
import (
+ "os"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
)
func TestBuildVariants(t *testing.T) {
@@ -173,7 +176,7 @@ hello:
hello:
other: "Bonjour"
-- layouts/index.html --
-{{ $options := dict "minify" false "externals" (slice "react" "react-dom") }}
+{{ $options := dict "minify" false "externals" (slice "react" "react-dom") "sourcemap" "linked" }}
{{ $js := resources.Get "js/main.js" | js.Build $options }}
JS: {{ template "print" $js }}
{{ $jsx := resources.Get "js/myjsx.jsx" | js.Build $options }}
@@ -201,14 +204,31 @@ TS2: {{ template "print" $ts2 }}
TxtarString: files,
}).Build()
- b.AssertFileContent("public/js/myts.js", `//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJz`)
- b.AssertFileContent("public/js/myts2.js.map", `"version": 3,`)
+ b.AssertFileContent("public/js/main.js", `//# sourceMappingURL=main.js.map`)
+ b.AssertFileContent("public/js/main.js.map", `"version":3`, "! ns-hugo") // linked
+ b.AssertFileContent("public/js/myts.js", `//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJz`) // inline
b.AssertFileContent("public/index.html", `
console.log(&#34;included&#34;);
if (hasSpace.test(string))
var React = __toESM(__require(&#34;react&#34;));
function greeter(person) {
`)
+
+ checkMap := func(p string, expectLen int) {
+ s := b.FileContent(p)
+ sources := esbuild.SourcesFromSourceMap(s)
+ b.Assert(sources, qt.HasLen, expectLen)
+
+ // Check that all source files exist.
+ for _, src := range sources {
+ filename, ok := paths.UrlStringToFilename(src)
+ b.Assert(ok, qt.IsTrue)
+ _, err := os.Stat(filename)
+ b.Assert(err, qt.IsNil, qt.Commentf("src: %q", src))
+ }
+ }
+
+ checkMap("public/js/main.js.map", 4)
}
func TestBuildError(t *testing.T) {
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
deleted file mode 100644
index 8c271d032..000000000
--- a/resources/resource_transformers/js/options.go
+++ /dev/null
@@ -1,461 +0,0 @@
-// Copyright 2020 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 (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/identity"
- "github.com/spf13/afero"
-
- "github.com/evanw/esbuild/pkg/api"
-
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/media"
- "github.com/mitchellh/mapstructure"
-)
-
-const (
- nsImportHugo = "ns-hugo"
- nsParams = "ns-params"
-
- stdinImporter = "<stdin>"
-)
-
-// Options esbuild configuration
-type Options struct {
- // If not set, the source path will be used as the base target path.
- // Note that the target path's extension may change if the target MIME type
- // is different, e.g. when the source is TypeScript.
- TargetPath string
-
- // Whether to minify to output.
- Minify bool
-
- // Whether to write mapfiles
- SourceMap string
-
- // The language target.
- // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
- // Default is esnext.
- Target string
-
- // The output format.
- // One of: iife, cjs, esm
- // Default is to esm.
- Format string
-
- // External dependencies, e.g. "react".
- Externals []string
-
- // This option allows you to automatically replace a global variable with an import from another file.
- // The filenames must be relative to /assets.
- // See https://esbuild.github.io/api/#inject
- Inject []string
-
- // User defined symbols.
- Defines map[string]any
-
- // Maps a component import to another.
- Shims map[string]string
-
- // User defined params. Will be marshaled to JSON and available as "@params", e.g.
- // import * as params from '@params';
- Params any
-
- // What to use instead of React.createElement.
- JSXFactory string
-
- // What to use instead of React.Fragment.
- JSXFragment string
-
- // What to do about JSX syntax.
- // See https://esbuild.github.io/api/#jsx
- JSX string
-
- // Which library to use to automatically import JSX helper functions from. Only works if JSX is set to automatic.
- // See https://esbuild.github.io/api/#jsx-import-source
- JSXImportSource string
-
- // There is/was a bug in WebKit with severe performance issue with the tracking
- // of TDZ checks in JavaScriptCore.
- //
- // Enabling this flag removes the TDZ and `const` assignment checks and
- // may improve performance of larger JS codebases until the WebKit fix
- // is in widespread use.
- //
- // See https://bugs.webkit.org/show_bug.cgi?id=199866
- // Deprecated: This no longer have any effect and will be removed.
- // TODO(bep) remove. See https://github.com/evanw/esbuild/commit/869e8117b499ca1dbfc5b3021938a53ffe934dba
- AvoidTDZ bool
-
- mediaType media.Type
- outDir string
- contents string
- sourceDir string
- resolveDir string
- tsConfig string
-}
-
-func decodeOptions(m map[string]any) (Options, error) {
- var opts Options
-
- if err := mapstructure.WeakDecode(m, &opts); err != nil {
- return opts, err
- }
-
- if opts.TargetPath != "" {
- opts.TargetPath = paths.ToSlashTrimLeading(opts.TargetPath)
- }
-
- opts.Target = strings.ToLower(opts.Target)
- opts.Format = strings.ToLower(opts.Format)
-
- return opts, nil
-}
-
-var extensionToLoaderMap = map[string]api.Loader{
- ".js": api.LoaderJS,
- ".mjs": api.LoaderJS,
- ".cjs": api.LoaderJS,
- ".jsx": api.LoaderJSX,
- ".ts": api.LoaderTS,
- ".tsx": api.LoaderTSX,
- ".css": api.LoaderCSS,
- ".json": api.LoaderJSON,
- ".txt": api.LoaderText,
-}
-
-func loaderFromFilename(filename string) api.Loader {
- l, found := extensionToLoaderMap[filepath.Ext(filename)]
- if found {
- return l
- }
- return api.LoaderJS
-}
-
-func resolveComponentInAssets(fs afero.Fs, impPath string) *hugofs.FileMeta {
- findFirst := func(base string) *hugofs.FileMeta {
- // This is the most common sub-set of ESBuild's default extensions.
- // We assume that imports of JSON, CSS etc. will be using their full
- // name with extension.
- for _, ext := range []string{".js", ".ts", ".tsx", ".jsx"} {
- if strings.HasSuffix(impPath, ext) {
- // Import of foo.js.js need the full name.
- continue
- }
- if fi, err := fs.Stat(base + ext); err == nil {
- return fi.(hugofs.FileMetaInfo).Meta()
- }
- }
-
- // Not found.
- return nil
- }
-
- var m *hugofs.FileMeta
-
- // We need to check if this is a regular file imported without an extension.
- // There may be ambiguous situations where both foo.js and foo/index.js exists.
- // This import order is in line with both how Node and ESBuild's native
- // import resolver works.
-
- // It may be a regular file imported without an extension, e.g.
- // foo or foo/index.
- m = findFirst(impPath)
- if m != nil {
- return m
- }
-
- base := filepath.Base(impPath)
- if base == "index" {
- // try index.esm.js etc.
- m = findFirst(impPath + ".esm")
- if m != nil {
- return m
- }
- }
-
- // Check the path as is.
- fi, err := fs.Stat(impPath)
-
- if err == nil {
- if fi.IsDir() {
- m = findFirst(filepath.Join(impPath, "index"))
- if m == nil {
- m = findFirst(filepath.Join(impPath, "index.esm"))
- }
- } else {
- m = fi.(hugofs.FileMetaInfo).Meta()
- }
- } else if strings.HasSuffix(base, ".js") {
- m = findFirst(strings.TrimSuffix(impPath, ".js"))
- }
-
- return m
-}
-
-func createBuildPlugins(depsManager identity.Manager, c *Client, opts Options) ([]api.Plugin, error) {
- fs := c.rs.Assets
-
- resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
- impPath := args.Path
- if opts.Shims != nil {
- override, found := opts.Shims[impPath]
- if found {
- impPath = override
- }
- }
- isStdin := args.Importer == stdinImporter
- var relDir string
- if !isStdin {
- rel, found := fs.MakePathRelative(args.Importer, true)
- if !found {
- // Not in any of the /assets folders.
- // This is an import from a node_modules, let
- // ESBuild resolve this.
- return api.OnResolveResult{}, nil
- }
-
- relDir = filepath.Dir(rel)
- } else {
- relDir = opts.sourceDir
- }
-
- // Imports not starting with a "." is assumed to live relative to /assets.
- // Hugo makes no assumptions about the directory structure below /assets.
- if relDir != "" && strings.HasPrefix(impPath, ".") {
- impPath = filepath.Join(relDir, impPath)
- }
-
- m := resolveComponentInAssets(fs.Fs, impPath)
-
- if m != nil {
- depsManager.AddIdentity(m.PathInfo)
-
- // Store the source root so we can create a jsconfig.json
- // to help IntelliSense when the build is done.
- // This should be a small number of elements, and when
- // in server mode, we may get stale entries on renames etc.,
- // but that shouldn't matter too much.
- c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot)
- return api.OnResolveResult{Path: m.Filename, Namespace: nsImportHugo}, nil
- }
-
- // Fall back to ESBuild's resolve.
- return api.OnResolveResult{}, nil
- }
-
- importResolver := api.Plugin{
- Name: "hugo-import-resolver",
- Setup: func(build api.PluginBuild) {
- build.OnResolve(api.OnResolveOptions{Filter: `.*`},
- func(args api.OnResolveArgs) (api.OnResolveResult, error) {
- return resolveImport(args)
- })
- build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsImportHugo},
- func(args api.OnLoadArgs) (api.OnLoadResult, error) {
- b, err := os.ReadFile(args.Path)
- if err != nil {
- return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
- }
- c := string(b)
- return api.OnLoadResult{
- // See https://github.com/evanw/esbuild/issues/502
- // This allows all modules to resolve dependencies
- // in the main project's node_modules.
- ResolveDir: opts.resolveDir,
- Contents: &c,
- Loader: loaderFromFilename(args.Path),
- }, nil
- })
- },
- }
-
- params := opts.Params
- if params == nil {
- // This way @params will always resolve to something.
- params = make(map[string]any)
- }
-
- b, err := json.Marshal(params)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal params: %w", err)
- }
- bs := string(b)
- paramsPlugin := api.Plugin{
- Name: "hugo-params-plugin",
- Setup: func(build api.PluginBuild) {
- build.OnResolve(api.OnResolveOptions{Filter: `^@params$`},
- func(args api.OnResolveArgs) (api.OnResolveResult, error) {
- return api.OnResolveResult{
- Path: args.Path,
- Namespace: nsParams,
- }, nil
- })
- build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsParams},
- func(args api.OnLoadArgs) (api.OnLoadResult, error) {
- return api.OnLoadResult{
- Contents: &bs,
- Loader: api.LoaderJSON,
- }, nil
- })
- },
- }
-
- return []api.Plugin{importResolver, paramsPlugin}, nil
-}
-
-func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
- var target api.Target
- switch opts.Target {
- case "", "esnext":
- target = api.ESNext
- case "es5":
- target = api.ES5
- case "es6", "es2015":
- target = api.ES2015
- case "es2016":
- target = api.ES2016
- case "es2017":
- target = api.ES2017
- case "es2018":
- target = api.ES2018
- case "es2019":
- target = api.ES2019
- case "es2020":
- target = api.ES2020
- case "es2021":
- target = api.ES2021
- case "es2022":
- target = api.ES2022
- case "es2023":
- target = api.ES2023
- default:
- err = fmt.Errorf("invalid target: %q", opts.Target)
- return
- }
-
- mediaType := opts.mediaType
- if mediaType.IsZero() {
- mediaType = media.Builtin.JavascriptType
- }
-
- var loader api.Loader
- switch mediaType.SubType {
- // TODO(bep) ESBuild support a set of other loaders, but I currently fail
- // to see the relevance. That may change as we start using this.
- case media.Builtin.JavascriptType.SubType:
- loader = api.LoaderJS
- case media.Builtin.TypeScriptType.SubType:
- loader = api.LoaderTS
- case media.Builtin.TSXType.SubType:
- loader = api.LoaderTSX
- case media.Builtin.JSXType.SubType:
- loader = api.LoaderJSX
- default:
- err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
- return
- }
-
- var format api.Format
- // One of: iife, cjs, esm
- switch opts.Format {
- case "", "iife":
- format = api.FormatIIFE
- case "esm":
- format = api.FormatESModule
- case "cjs":
- format = api.FormatCommonJS
- default:
- err = fmt.Errorf("unsupported script output format: %q", opts.Format)
- return
- }
-
- var jsx api.JSX
- switch opts.JSX {
- case "", "transform":
- jsx = api.JSXTransform
- case "preserve":
- jsx = api.JSXPreserve
- case "automatic":
- jsx = api.JSXAutomatic
- default:
- err = fmt.Errorf("unsupported jsx type: %q", opts.JSX)
- return
- }
-
- var defines map[string]string
- if opts.Defines != nil {
- defines = maps.ToStringMapString(opts.Defines)
- }
-
- // By default we only need to specify outDir and no outFile
- outDir := opts.outDir
- outFile := ""
- var sourceMap api.SourceMap
- switch opts.SourceMap {
- case "inline":
- sourceMap = api.SourceMapInline
- case "external":
- sourceMap = api.SourceMapExternal
- case "":
- sourceMap = api.SourceMapNone
- default:
- err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
- return
- }
-
- buildOptions = api.BuildOptions{
- Outfile: outFile,
- Bundle: true,
-
- Target: target,
- Format: format,
- Sourcemap: sourceMap,
-
- MinifyWhitespace: opts.Minify,
- MinifyIdentifiers: opts.Minify,
- MinifySyntax: opts.Minify,
-
- Outdir: outDir,
- Define: defines,
-
- External: opts.Externals,
-
- JSXFactory: opts.JSXFactory,
- JSXFragment: opts.JSXFragment,
-
- JSX: jsx,
- JSXImportSource: opts.JSXImportSource,
-
- Tsconfig: opts.tsConfig,
-
- // Note: We're not passing Sourcefile to ESBuild.
- // This makes ESBuild pass `stdin` as the Importer to the import
- // resolver, which is what we need/expect.
- Stdin: &api.StdinOptions{
- Contents: opts.contents,
- ResolveDir: opts.resolveDir,
- Loader: loader,
- },
- }
- return
-}
diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go
deleted file mode 100644
index 53aa9b6bb..000000000
--- a/resources/resource_transformers/js/options_test.go
+++ /dev/null
@@ -1,241 +0,0 @@
-// Copyright 2020 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 (
- "path"
- "path/filepath"
- "testing"
-
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/hugolib/filesystems"
- "github.com/gohugoio/hugo/hugolib/paths"
- "github.com/gohugoio/hugo/media"
-
- "github.com/spf13/afero"
-
- "github.com/evanw/esbuild/pkg/api"
-
- qt "github.com/frankban/quicktest"
-)
-
-// This test is added to test/warn against breaking the "stability" of the
-// cache key. It's sometimes needed to break this, but should be avoided if possible.
-func TestOptionKey(t *testing.T) {
- c := qt.New(t)
-
- opts := map[string]any{
- "TargetPath": "foo",
- "Target": "es2018",
- }
-
- key := (&buildTransformation{optsm: opts}).Key()
-
- c.Assert(key.Value(), qt.Equals, "jsbuild_1533819657654811600")
-}
-
-func TestToBuildOptions(t *testing.T) {
- c := qt.New(t)
-
- opts, err := toBuildOptions(Options{mediaType: media.Builtin.JavascriptType})
-
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ESNext,
- Format: api.FormatIIFE,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018",
- Format: "cjs",
- Minify: true,
- mediaType: media.Builtin.JavascriptType,
- AvoidTDZ: true,
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType,
- SourceMap: "inline",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Sourcemap: api.SourceMapInline,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType,
- SourceMap: "inline",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Sourcemap: api.SourceMapInline,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType,
- SourceMap: "external",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Sourcemap: api.SourceMapExternal,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- mediaType: media.Builtin.JavascriptType,
- JSX: "automatic", JSXImportSource: "preact",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ESNext,
- Format: api.FormatIIFE,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- JSX: api.JSXAutomatic,
- JSXImportSource: "preact",
- })
-}
-
-func TestToBuildOptionsTarget(t *testing.T) {
- c := qt.New(t)
-
- for _, test := range []struct {
- target string
- expect api.Target
- }{
- {"es2015", api.ES2015},
- {"es2016", api.ES2016},
- {"es2017", api.ES2017},
- {"es2018", api.ES2018},
- {"es2019", api.ES2019},
- {"es2020", api.ES2020},
- {"es2021", api.ES2021},
- {"es2022", api.ES2022},
- {"es2023", api.ES2023},
- {"", api.ESNext},
- {"esnext", api.ESNext},
- } {
- c.Run(test.target, func(c *qt.C) {
- opts, err := toBuildOptions(Options{
- Target: test.target,
- mediaType: media.Builtin.JavascriptType,
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts.Target, qt.Equals, test.expect)
- })
- }
-}
-
-func TestResolveComponentInAssets(t *testing.T) {
- c := qt.New(t)
-
- for _, test := range []struct {
- name string
- files []string
- impPath string
- expect string
- }{
- {"Basic, extension", []string{"foo.js", "bar.js"}, "foo.js", "foo.js"},
- {"Basic, no extension", []string{"foo.js", "bar.js"}, "foo", "foo.js"},
- {"Basic, no extension, typescript", []string{"foo.ts", "bar.js"}, "foo", "foo.ts"},
- {"Not found", []string{"foo.js", "bar.js"}, "moo.js", ""},
- {"Not found, double js extension", []string{"foo.js.js", "bar.js"}, "foo.js", ""},
- {"Index file, folder only", []string{"foo/index.js", "bar.js"}, "foo", "foo/index.js"},
- {"Index file, folder and index", []string{"foo/index.js", "bar.js"}, "foo/index", "foo/index.js"},
- {"Index file, folder and index and suffix", []string{"foo/index.js", "bar.js"}, "foo/index.js", "foo/index.js"},
- {"Index ESM file, folder only", []string{"foo/index.esm.js", "bar.js"}, "foo", "foo/index.esm.js"},
- {"Index ESM file, folder and index", []string{"foo/index.esm.js", "bar.js"}, "foo/index", "foo/index.esm.js"},
- {"Index ESM file, folder and index and suffix", []string{"foo/index.esm.js", "bar.js"}, "foo/index.esm.js", "foo/index.esm.js"},
- // We added these index.esm.js cases in v0.101.0. The case below is unlikely to happen in the wild, but add a test
- // to document Hugo's behavior. We pick the file with the name index.js; anything else would be breaking.
- {"Index and Index ESM file, folder only", []string{"foo/index.esm.js", "foo/index.js", "bar.js"}, "foo", "foo/index.js"},
-
- // Issue #8949
- {"Check file before directory", []string{"foo.js", "foo/index.js"}, "foo", "foo.js"},
- } {
- c.Run(test.name, func(c *qt.C) {
- baseDir := "assets"
- mfs := afero.NewMemMapFs()
-
- for _, filename := range test.files {
- c.Assert(afero.WriteFile(mfs, filepath.Join(baseDir, filename), []byte("let foo='bar';"), 0o777), qt.IsNil)
- }
-
- conf := testconfig.GetTestConfig(mfs, config.New())
- fs := hugofs.NewFrom(mfs, conf.BaseConfig())
-
- p, err := paths.New(fs, conf)
- c.Assert(err, qt.IsNil)
- bfs, err := filesystems.NewBase(p, nil)
- c.Assert(err, qt.IsNil)
-
- got := resolveComponentInAssets(bfs.Assets.Fs, test.impPath)
-
- gotPath := ""
- expect := test.expect
- if got != nil {
- gotPath = filepath.ToSlash(got.Filename)
- expect = path.Join(baseDir, test.expect)
- }
-
- c.Assert(gotPath, qt.Equals, expect)
- })
- }
-}
diff --git a/resources/resource_transformers/js/transform.go b/resources/resource_transformers/js/transform.go
new file mode 100644
index 000000000..13909e54c
--- /dev/null
+++ b/resources/resource_transformers/js/transform.go
@@ -0,0 +1,68 @@
+// 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 (
+ "io"
+ "path"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/internal/js/esbuild"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/internal"
+)
+
+type buildTransformation struct {
+ optsm map[string]any
+ c *Client
+}
+
+func (t *buildTransformation) Key() internal.ResourceTransformationKey {
+ return internal.NewResourceTransformationKey("jsbuild", t.optsm)
+}
+
+func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+ ctx.OutMediaType = media.Builtin.JavascriptType
+
+ var opts esbuild.Options
+
+ if t.optsm != nil {
+ optsExt, err := esbuild.DecodeExternalOptions(t.optsm)
+ if err != nil {
+ return err
+ }
+ opts.ExternalOptions = optsExt
+ }
+
+ if opts.TargetPath != "" {
+ ctx.OutPath = opts.TargetPath
+ } else {
+ ctx.ReplaceOutPathExtension(".js")
+ }
+
+ src, err := io.ReadAll(ctx.From)
+ if err != nil {
+ return err
+ }
+
+ opts.SourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
+ opts.Contents = string(src)
+ opts.MediaType = ctx.InMediaType
+ opts.Stdin = true
+
+ _, err = t.c.transform(opts, ctx)
+
+ return err
+}
diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go
index 0d7b57062..77bacc115 100644
--- a/resources/resource_transformers/tocss/dartsass/transform.go
+++ b/resources/resource_transformers/tocss/dartsass/transform.go
@@ -139,7 +139,7 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
return url, nil
}
- filePath, isURL := paths.UrlToFilename(url)
+ filePath, isURL := paths.UrlStringToFilename(url)
var prevDir string
var pathDir string
if isURL {
@@ -195,7 +195,7 @@ func (t importResolver) Load(url string) (godartsass.Import, error) {
if url == sass.HugoVarsNamespace {
return t.varsStylesheet, nil
}
- filename, _ := paths.UrlToFilename(url)
+ filename, _ := paths.UrlStringToFilename(url)
b, err := afero.ReadFile(hugofs.Os, filename)
sourceSyntax := godartsass.SourceSyntaxSCSS
diff --git a/resources/transform.go b/resources/transform.go
index 336495e6d..4214067bd 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -52,6 +52,8 @@ var (
_ identity.IdentityGroupProvider = (*resourceAdapterInner)(nil)
_ resource.Source = (*resourceAdapter)(nil)
_ resource.Identifier = (*resourceAdapter)(nil)
+ _ targetPathProvider = (*resourceAdapter)(nil)
+ _ sourcePathProvider = (*resourceAdapter)(nil)
_ resource.ResourceNameTitleProvider = (*resourceAdapter)(nil)
_ resource.WithResourceMetaProvider = (*resourceAdapter)(nil)
_ identity.DependencyManagerProvider = (*resourceAdapter)(nil)
@@ -277,6 +279,19 @@ func (r *resourceAdapter) Key() string {
return r.target.(resource.Identifier).Key()
}
+func (r *resourceAdapter) targetPath() string {
+ r.init(false, false)
+ return r.target.(targetPathProvider).targetPath()
+}
+
+func (r *resourceAdapter) sourcePath() string {
+ r.init(false, false)
+ if sp, ok := r.target.(sourcePathProvider); ok {
+ return sp.sourcePath()
+ }
+ return ""
+}
+
func (r *resourceAdapter) MediaType() media.Type {
r.init(false, false)
return r.target.MediaType()