Skip to content

Commit ae1228d

Browse files
committed
Add resource vendoring
Fixes #13309
1 parent e08d9af commit ae1228d

File tree

18 files changed

+332
-33
lines changed

18 files changed

+332
-33
lines changed

Diff for: commands/commandeer.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,11 @@ func (r *rootCommand) Name() string {
365365
}
366366

367367
func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
368-
b := newHugoBuilder(r, nil)
368+
var vendor bool
369+
if vendorCmd, ok := cd.Command.(vendoredCommand); ok {
370+
vendor = vendorCmd.IsVendorCommand()
371+
}
372+
b := newHugoBuilder(r, nil, vendor)
369373

370374
if !r.buildWatch {
371375
defer b.postBuild("Total", time.Now())

Diff for: commands/commands.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import (
2323
func newExec() (*simplecobra.Exec, error) {
2424
rootCmd := &rootCommand{
2525
commands: []simplecobra.Commander{
26-
newHugoBuildCmd(),
26+
newHugoBuildCmd(false),
27+
newHugoBuildCmd(true),
2728
newVersionCmd(),
2829
newEnvCommand(),
2930
newServerCommand(),
@@ -42,26 +43,40 @@ func newExec() (*simplecobra.Exec, error) {
4243
return simplecobra.New(rootCmd)
4344
}
4445

45-
func newHugoBuildCmd() simplecobra.Commander {
46-
return &hugoBuildCommand{}
46+
func newHugoBuildCmd(vendor bool) simplecobra.Commander {
47+
return &hugoBuildCommand{
48+
vendor: vendor,
49+
}
4750
}
4851

4952
// hugoBuildCommand just delegates to the rootCommand.
5053
type hugoBuildCommand struct {
5154
rootCmd *rootCommand
55+
vendor bool
5256
}
5357

5458
func (c *hugoBuildCommand) Commands() []simplecobra.Commander {
5559
return nil
5660
}
5761

5862
func (c *hugoBuildCommand) Name() string {
63+
if c.vendor {
64+
return "vendor"
65+
}
5966
return "build"
6067
}
6168

69+
type vendoredCommand interface {
70+
IsVendorCommand() bool
71+
}
72+
73+
func (c *hugoBuildCommand) IsVendorCommand() bool {
74+
return c.vendor
75+
}
76+
6277
func (c *hugoBuildCommand) Init(cd *simplecobra.Commandeer) error {
6378
c.rootCmd = cd.Root.Command.(*rootCommand)
64-
return c.rootCmd.initRootCommand("build", cd)
79+
return c.rootCmd.initRootCommand(c.Name(), cd)
6580
}
6681

6782
func (c *hugoBuildCommand) PreRun(cd, runner *simplecobra.Commandeer) error {

Diff for: commands/hugobuilder.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ type hugoBuilder struct {
7373
showErrorInBrowser bool
7474

7575
errState hugoBuilderErrState
76+
77+
vendor bool
7678
}
7779

7880
var errConfigNotSet = errors.New("config not set")
@@ -1046,11 +1048,11 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
10461048
}
10471049
}
10481050
cfg.Set("environment", c.r.environment)
1049-
10501051
cfg.Set("internal", maps.Params{
10511052
"running": running,
10521053
"watch": watch,
10531054
"verbose": c.r.isVerbose(),
1055+
"vendor": c.vendor,
10541056
"fastRenderMode": c.fastRenderMode,
10551057
})
10561058

Diff for: commands/server.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,15 @@ const (
8484
configChangeGoWork = "go work file"
8585
)
8686

87-
func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
87+
func newHugoBuilder(r *rootCommand, s *serverCommand, vendor bool, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
8888
var visitedURLs *types.EvictingQueue[string]
8989
if s != nil && !s.disableFastRender {
9090
visitedURLs = types.NewEvictingQueue[string](20)
9191
}
9292
return &hugoBuilder{
9393
r: r,
9494
s: s,
95+
vendor: vendor,
9596
visitedURLs: visitedURLs,
9697
fullRebuildSem: semaphore.NewWeighted(1),
9798
debounce: debounce.New(4 * time.Second),
@@ -563,6 +564,7 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
563564
c.hugoBuilder = newHugoBuilder(
564565
c.r,
565566
c,
567+
false,
566568
func(reloaded bool) error {
567569
if !reloaded {
568570
if err := c.createServerPorts(cd); err != nil {

Diff for: common/hugo/hugo.go

+6
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ func (i HugoInfo) IsExtended() bool {
109109
return IsExtended
110110
}
111111

112+
// IsVendor returns whether we're running as `hugo vendor`.
113+
func (i HugoInfo) IsVendor() bool {
114+
return i.conf.Vendor()
115+
}
116+
112117
// WorkingDir returns the project working directory.
113118
func (i HugoInfo) WorkingDir() string {
114119
return i.conf.WorkingDir()
@@ -166,6 +171,7 @@ type ConfigProvider interface {
166171
WorkingDir() string
167172
IsMultihost() bool
168173
IsMultilingual() bool
174+
Vendor() bool
169175
}
170176

171177
// NewInfo creates a new Hugo Info object.

Diff for: config/allconfig/allconfig.go

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type InternalConfig struct {
6969
Watch bool
7070
FastRenderMode bool
7171
LiveReloadPort int
72+
Vendor bool
7273
}
7374

7475
// All non-params config keys for language.

Diff for: config/allconfig/configlanguage.go

+4
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ func (c ConfigLanguage) Watching() bool {
137137
return c.m.Base.Internal.Watch
138138
}
139139

140+
func (c ConfigLanguage) Vendor() bool {
141+
return c.m.Base.Internal.Vendor
142+
}
143+
140144
func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager {
141145
if !c.Watching() {
142146
return identity.NopManager

Diff for: config/configProvider.go

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type AllProvider interface {
5858
BuildDrafts() bool
5959
Running() bool
6060
Watching() bool
61+
Vendor() bool
6162
NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager
6263
FastRenderMode() bool
6364
PrintUnusedTemplates() bool

Diff for: hugolib/integrationtest_builder.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -665,17 +665,18 @@ func (s *IntegrationTestBuilder) initBuilder() error {
665665
flags = config.New()
666666
}
667667

668+
internal := make(maps.Params)
669+
668670
if s.Cfg.Running {
669-
flags.Set("internal", maps.Params{
670-
"running": s.Cfg.Running,
671-
"watch": s.Cfg.Running,
672-
})
671+
internal["running"] = true
672+
internal["watch"] = true
673+
673674
} else if s.Cfg.Watching {
674-
flags.Set("internal", maps.Params{
675-
"watch": s.Cfg.Watching,
676-
})
675+
internal["watch"] = true
677676
}
678677

678+
flags.Set("internal", internal)
679+
679680
if s.Cfg.WorkingDir != "" {
680681
flags.Set("workingDir", s.Cfg.WorkingDir)
681682
}

Diff for: internal/js/esbuild/build.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ import (
3434
// NewBuildClient creates a new BuildClient.
3535
func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec) *BuildClient {
3636
return &BuildClient{
37-
rs: rs,
37+
Rs: rs,
3838
sfs: fs,
3939
}
4040
}
4141

4242
// BuildClient is a client for building JavaScript resources using esbuild.
4343
type BuildClient struct {
44-
rs *resources.Spec
44+
Rs *resources.Spec
4545
sfs *filesystems.SourceFilesystem
4646
}
4747

@@ -52,11 +52,11 @@ func (c *BuildClient) Build(opts Options) (api.BuildResult, error) {
5252
dependencyManager = identity.NopManager
5353
}
5454

55-
opts.OutDir = c.rs.AbsPublishDir
56-
opts.ResolveDir = c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
55+
opts.OutDir = c.Rs.AbsPublishDir
56+
opts.ResolveDir = c.Rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
5757
opts.AbsWorkingDir = opts.ResolveDir
58-
opts.TsConfig = c.rs.ResolveJSConfigFile("tsconfig.json")
59-
assetsResolver := newFSResolver(c.rs.Assets.Fs)
58+
opts.TsConfig = c.Rs.ResolveJSConfigFile("tsconfig.json")
59+
assetsResolver := newFSResolver(c.Rs.Assets.Fs)
6060

6161
if err := opts.validate(); err != nil {
6262
return api.BuildResult{}, err
@@ -67,7 +67,7 @@ func (c *BuildClient) Build(opts Options) (api.BuildResult, error) {
6767
}
6868

6969
var err error
70-
opts.compiled.Plugins, err = createBuildPlugins(c.rs, assetsResolver, dependencyManager, opts)
70+
opts.compiled.Plugins, err = createBuildPlugins(c.Rs, assetsResolver, dependencyManager, opts)
7171
if err != nil {
7272
return api.BuildResult{}, err
7373
}
@@ -175,7 +175,7 @@ func (c *BuildClient) Build(opts Options) (api.BuildResult, error) {
175175
// Return 1, log the rest.
176176
for i, err := range errors {
177177
if i > 0 {
178-
c.rs.Logger.Errorf("js.Build failed: %s", err)
178+
c.Rs.Logger.Errorf("js.Build failed: %s", err)
179179
}
180180
}
181181

Diff for: resources/internal/vendor/vendor.go

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package vendor
15+
16+
import (
17+
"io"
18+
"path/filepath"
19+
20+
"github.com/cespare/xxhash/v2"
21+
"github.com/gohugoio/hugo/common/herrors"
22+
"github.com/gohugoio/hugo/common/hugio"
23+
"github.com/gohugoio/hugo/helpers"
24+
"github.com/spf13/afero"
25+
)
26+
27+
var _ io.Closer = (*ResourceVendor)(nil)
28+
29+
type ResourceVendor struct {
30+
// For the from hash.
31+
Digest *xxhash.Digest
32+
33+
closeFunc func() error
34+
35+
VendoredFile hugio.ReadSeekCloser
36+
37+
// TODO1 names.
38+
FinalFrom io.Reader
39+
FinalTo io.Writer
40+
}
41+
42+
func (v *ResourceVendor) Close() error {
43+
if v.closeFunc != nil {
44+
return v.closeFunc()
45+
}
46+
return nil
47+
}
48+
49+
func NewVendorer(fs afero.Fs, workingDir string) *Vendorer {
50+
return &Vendorer{
51+
fs: fs,
52+
workingDir: workingDir,
53+
}
54+
}
55+
56+
type Vendorer struct {
57+
fs afero.Fs
58+
workingDir string
59+
}
60+
61+
type ResourceVendorOptions struct {
62+
Name string
63+
InPath string
64+
From io.Reader
65+
To io.Writer
66+
}
67+
68+
const (
69+
vendorRoot = "_vendor"
70+
vendorModules = "mod"
71+
vendorResources = "res"
72+
vendorScopeAll = "_all"
73+
)
74+
75+
// TODO1 add --vendorScope=* or {development,production} e.g. Encode it into the root folder. Or: At least create a "all" root folder for future use.
76+
// add resources.txt to root with file/hash listing + version.
77+
func (v *Vendorer) NewResourceVendor(opts ResourceVendorOptions) (*ResourceVendor, error) {
78+
digest := xxhash.New()
79+
finalFrom := io.TeeReader(opts.From, digest)
80+
vendorFileName := filepath.Join(v.workingDir, vendorRoot, vendorResources, vendorScopeAll, opts.Name, opts.InPath)
81+
vendoredFile, err := helpers.OpenFileForWriting(v.fs, vendorFileName)
82+
if err != nil {
83+
return nil, err
84+
}
85+
finalTo := io.MultiWriter(opts.To, vendoredFile)
86+
return &ResourceVendor{
87+
Digest: digest,
88+
FinalFrom: finalFrom,
89+
FinalTo: finalTo,
90+
closeFunc: vendoredFile.Close,
91+
}, nil
92+
}
93+
94+
// OpenVendoredFile opens a vendored file for reading or nil if not found.
95+
func (v *Vendorer) OpenVendoredFile(opts ResourceVendorOptions) (hugio.ReadSeekCloser, error) {
96+
vendorFileName := v.vendorFilename(opts)
97+
f, err := v.fs.Open(vendorFileName)
98+
if err != nil {
99+
if herrors.IsNotExist(err) {
100+
return nil, nil
101+
}
102+
return nil, err
103+
}
104+
return f, nil
105+
}
106+
107+
func (v *Vendorer) vendorFilename(opts ResourceVendorOptions) string {
108+
return filepath.Join(v.workingDir, vendorRoot, vendorResources, vendorScopeAll, opts.Name, opts.InPath)
109+
}

Diff for: resources/internal/vendor/vendor_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package vendor
15+
16+
import "testing"
17+
18+
func TestResourceVendor(t *testing.T) {
19+
}

Diff for: resources/resource_spec.go

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/gohugoio/hugo/config/allconfig"
2323
"github.com/gohugoio/hugo/output"
2424
"github.com/gohugoio/hugo/resources/internal"
25+
"github.com/gohugoio/hugo/resources/internal/vendor"
2526
"github.com/gohugoio/hugo/resources/jsconfig"
2627
"github.com/gohugoio/hugo/resources/page/pagemeta"
2728

@@ -103,6 +104,7 @@ func NewSpec(
103104
memCache,
104105
s,
105106
),
107+
Vendorer: vendor.NewVendorer(s.SourceFs, s.Cfg.WorkingDir()),
106108
ExecHelper: execHelper,
107109

108110
Permalinks: permalinks,
@@ -122,6 +124,7 @@ type Spec struct {
122124
ErrorSender herrors.ErrorSender
123125
BuildClosers types.CloseAdder
124126
Rebuilder identity.SignalRebuilder
127+
Vendorer *vendor.Vendorer
125128

126129
TextTemplates tpl.TemplateParseFinder
127130

0 commit comments

Comments
 (0)