mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
npm: Use workspaces to simplify hugo mod npm pack
Rewrite `hugo mod npm pack` to use npm workspaces. Module deps are now
written to packages/hugoautogen/package.json and the root package.json
gets a "workspaces" reference. A hugo_packagemeta.json sidecar stores a
hash of all input package files so regular commands can warn when npm
deps are out of sync.
Other changes:
- Workspace glob patterns (*, **, {a,b}) are resolved via gobwas/glob.
- Workspaces defined in package.hugo.json are supported.
- package.hugo.json is only recognised at module roots, not in workspaces.
- When package.hugo.json exists, package.json is not mounted or vendored.
- packages/hugoautogen is not mounted or vendored from dependencies.
- Add usePackageJSON import option (auto/always/never) to control whether
a module's npm deps are included. "auto" checks for Hugo config files
or package.hugo.json.
- The staleness check is skipped when running `hugo mod npm pack` itself.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
3ff9b7f89f
commit
d88a29e002
@@ -97,6 +97,7 @@ type commonConfig struct {
|
||||
type configKey struct {
|
||||
counter int32
|
||||
ignoreModulesDoesNotExists bool
|
||||
skipNpmCheck bool
|
||||
}
|
||||
|
||||
// This is the root command.
|
||||
@@ -195,6 +196,7 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
|
||||
Logger: r.logger,
|
||||
Environment: r.environment,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
SkipNpmCheck: key.skipNpmCheck,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -251,6 +253,7 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
|
||||
Environment: r.environment,
|
||||
Logger: r.logger,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
SkipNpmCheck: key.skipNpmCheck,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
+21
-11
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -49,28 +50,34 @@ func newModCommands() *modCommands {
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "pack",
|
||||
short: "Experimental: Prepares and writes a composite package.json file for your project",
|
||||
long: `Prepares and writes a composite package.json file for your project.
|
||||
short: "Merges module npm dependencies into an npm workspace",
|
||||
long: `Merges npm dependencies from all Hugo modules into a "packages/hugoautogen" npm workspace.
|
||||
|
||||
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
|
||||
with the base dependency set.
|
||||
The merged dependencies are written to packages/hugoautogen/package.json, and the root package.json
|
||||
is updated with a "workspaces" entry pointing to "packages/hugoautogen".
|
||||
|
||||
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
|
||||
The source entries are read from either package.hugo.json or package.json in the module root, with package.hugo.json taking precedence if both exist.
|
||||
|
||||
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
|
||||
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
|
||||
so this may/will change in future versions of Hugo.
|
||||
See [npm dependencies](/hugo-modules/npm-dependencies/) for more information.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
cfg := flagsToCfg(cd, nil)
|
||||
k := configKey{counter: r.configVersionID.Load(), skipNpmCheck: true}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
conf, err := r.ConfigFromProvider(key, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hugolib.NewHugoSites(r.newDepsConfig(conf))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs, h.Configs.Modules)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,7 +300,10 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
return err
|
||||
}
|
||||
client := conf.configs.ModulesClient
|
||||
return client.Get(args...)
|
||||
if err := client.Get(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -286,7 +286,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli)
|
||||
fmt.Fprintf(l.stdErr, "%s in %v ms\n", name, milli)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Printf(format string, v ...any) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
hglob "github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
@@ -95,6 +96,10 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
|
||||
configs.Modules = moduleConfig.AllModules
|
||||
configs.ModulesClient = modulesClient
|
||||
|
||||
if !d.SkipNpmCheck && npm.NpmPackNeedsUpdate(d.Fs, configs.Modules) {
|
||||
d.Logger.Warnln(`npm dependencies are out of sync, please run "hugo mod npm pack" (you may also want to run "npm install" after that)`)
|
||||
}
|
||||
|
||||
if err := configs.Init(d.Fs, d.Logger); err != nil {
|
||||
return nil, fmt.Errorf("failed to init config: %w", err)
|
||||
}
|
||||
@@ -127,6 +132,9 @@ type ConfigSourceDescriptor struct {
|
||||
|
||||
// If set, this will be used to ignore the module does not exist error.
|
||||
IgnoreModuleDoesNotExist bool
|
||||
|
||||
// If set, skip the npm pack staleness check (used by hugo mod npm pack).
|
||||
SkipNpmCheck bool
|
||||
}
|
||||
|
||||
func (d ConfigSourceDescriptor) configFilenames() []string {
|
||||
|
||||
@@ -21,14 +21,18 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// The NPM package.json "template" file.
|
||||
// When merging, we check for this file first, and then package.json.
|
||||
FilenamePackageHugoJSON = "package.hugo.json"
|
||||
// The NPM package file.
|
||||
FilenamePackageJSON = "package.json"
|
||||
|
||||
FilenameHugoStatsJSON = "hugo_stats.json"
|
||||
FilenameHugoStatsJSON = "hugo_stats.json"
|
||||
FilenamePackageMetaJSON = "hugo_packagemeta.json"
|
||||
)
|
||||
|
||||
// FolderPackagesHugoAutoGen is the auto-generated npm workspace directory for Hugo module deps.
|
||||
var FolderPackagesHugoAutoGen = filepath.Join("packages", "hugoautogen")
|
||||
|
||||
func IsGoTmplExt(ext string) bool {
|
||||
return ext == "gotmpl"
|
||||
}
|
||||
|
||||
@@ -979,6 +979,7 @@ type goModule struct {
|
||||
Time *time.Time // time version was created
|
||||
Update *goModule // available update, if any (with -u)
|
||||
Sum string // checksum
|
||||
GoModSum string // checksum for go.mod
|
||||
Main bool // is this the main module?
|
||||
Indirect bool // is this module only an indirect dependency of main module?
|
||||
Dir string // directory holding files for this module, if any
|
||||
|
||||
+82
-1
@@ -15,6 +15,7 @@ package modules
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
@@ -677,10 +679,18 @@ func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([
|
||||
return mounts, fmt.Errorf("failed to read dir %q: %q", owner.Dir(), err)
|
||||
}
|
||||
|
||||
hasPackageHugoJSON := false
|
||||
for _, fi := range fis {
|
||||
if fi.Name() == files.FilenamePackageHugoJSON {
|
||||
hasPackageHugoJSON = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, fi := range fis {
|
||||
n := fi.Name()
|
||||
|
||||
should := n == files.FilenamePackageHugoJSON || n == files.FilenamePackageJSON
|
||||
should := n == files.FilenamePackageHugoJSON || (n == files.FilenamePackageJSON && !hasPackageHugoJSON)
|
||||
should = should || commonJSConfigs.MatchString(n)
|
||||
|
||||
if should {
|
||||
@@ -692,9 +702,80 @@ func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([
|
||||
|
||||
}
|
||||
|
||||
// Mount the project's hugoautogen workspace package.json (not from dependencies).
|
||||
if owner.projectMod {
|
||||
pkgAutoGen := filepath.Join(files.FolderPackagesHugoAutoGen, files.FilenamePackageJSON)
|
||||
if fi, err := c.fs.Stat(filepath.Join(owner.Dir(), pkgAutoGen)); err == nil && !fi.IsDir() {
|
||||
mounts = append(mounts, Mount{
|
||||
Source: pkgAutoGen,
|
||||
Target: filepath.Join(files.ComponentFolderAssets, files.FolderJSConfig, pkgAutoGen),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Mount workspace package.json files (skipping hugoautogen from dependencies).
|
||||
// Read workspaces from package.hugo.json if it exists, otherwise from package.json.
|
||||
pkgFile := files.FilenamePackageJSON
|
||||
if hasPackageHugoJSON {
|
||||
pkgFile = files.FilenamePackageHugoJSON
|
||||
}
|
||||
if pkgData, err := afero.ReadFile(c.fs, filepath.Join(owner.Dir(), pkgFile)); err == nil {
|
||||
var pkg map[string]any
|
||||
if err := json.Unmarshal(pkgData, &pkg); err == nil {
|
||||
for _, ws := range cast.ToStringSlice(pkg["workspaces"]) {
|
||||
wsDirs := ResolveWorkspacePattern(c.fs, owner.Dir(), ws)
|
||||
for _, wsDir := range wsDirs {
|
||||
if filepath.ToSlash(wsDir) == filepath.ToSlash(files.FolderPackagesHugoAutoGen) {
|
||||
continue
|
||||
}
|
||||
wsPackageJSON := filepath.Join(wsDir, files.FilenamePackageJSON)
|
||||
if fi, err := c.fs.Stat(filepath.Join(owner.Dir(), wsPackageJSON)); err == nil && !fi.IsDir() {
|
||||
mounts = append(mounts, Mount{
|
||||
Source: wsPackageJSON,
|
||||
Target: filepath.Join(files.ComponentFolderAssets, files.FolderJSConfig, wsPackageJSON),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
// ResolveWorkspacePattern resolves an npm workspace pattern to actual directory paths.
|
||||
// Supports globs (*, **) and brace expansion ({a,b}) via gobwas/glob.
|
||||
// Literal paths are returned as-is.
|
||||
func ResolveWorkspacePattern(fs afero.Fs, root, pattern string) []string {
|
||||
if !strings.ContainsAny(pattern, "*?[{}") {
|
||||
return []string{pattern}
|
||||
}
|
||||
|
||||
g, err := glob.Compile(pattern, '/')
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dirs []string
|
||||
_ = afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.Name() != files.FilenamePackageJSON {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(root, filepath.Dir(path))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if g.Match(filepath.ToSlash(rel)) {
|
||||
dirs = append(dirs, rel)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return dirs
|
||||
}
|
||||
|
||||
func (c *collector) nodeModulesRoot(s string) string {
|
||||
s = filepath.ToSlash(s)
|
||||
if strings.HasPrefix(s, "node_modules/") {
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestPathKey(t *testing.T) {
|
||||
@@ -36,6 +38,63 @@ func TestPathKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveWorkspacePattern(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
root := filepath.FromSlash("/project/mod")
|
||||
|
||||
// Create workspace package.json files.
|
||||
for _, dir := range []string{
|
||||
"packages/aws1",
|
||||
"packages/aws2",
|
||||
"packages/hugoautogen",
|
||||
"other/foo",
|
||||
} {
|
||||
p := filepath.Join(root, dir, "package.json")
|
||||
c.Assert(afero.WriteFile(fs, p, []byte(`{}`), 0o644), qt.IsNil)
|
||||
}
|
||||
|
||||
// Literal path, no glob.
|
||||
c.Assert(ResolveWorkspacePattern(fs, root, "packages/aws1"), qt.DeepEquals, []string{"packages/aws1"})
|
||||
|
||||
got := ResolveWorkspacePattern(fs, root, "packages/*")
|
||||
c.Assert(got, qt.DeepEquals, []string{
|
||||
filepath.FromSlash("packages/aws1"),
|
||||
filepath.FromSlash("packages/aws2"),
|
||||
filepath.FromSlash("packages/hugoautogen"),
|
||||
})
|
||||
|
||||
// We currently support only one level of globbing, so this should give the same result as above.
|
||||
got = ResolveWorkspacePattern(fs, root, "packages/**")
|
||||
c.Assert(got, qt.DeepEquals, []string{
|
||||
filepath.FromSlash("packages/aws1"),
|
||||
filepath.FromSlash("packages/aws2"),
|
||||
filepath.FromSlash("packages/hugoautogen"),
|
||||
})
|
||||
|
||||
got = ResolveWorkspacePattern(fs, root, "packages/{aws1,aws2}")
|
||||
c.Assert(got, qt.DeepEquals, []string{
|
||||
filepath.FromSlash("packages/aws1"),
|
||||
filepath.FromSlash("packages/aws2"),
|
||||
})
|
||||
|
||||
// ** matches recursively.
|
||||
nestedDir := filepath.Join(root, "packages", "aws1", "sub", "package.json")
|
||||
c.Assert(afero.WriteFile(fs, nestedDir, []byte(`{}`), 0o644), qt.IsNil)
|
||||
got = ResolveWorkspacePattern(fs, root, "packages/**")
|
||||
c.Assert(got, qt.DeepEquals, []string{
|
||||
filepath.FromSlash("packages/aws1"),
|
||||
filepath.FromSlash("packages/aws1/sub"),
|
||||
filepath.FromSlash("packages/aws2"),
|
||||
filepath.FromSlash("packages/hugoautogen"),
|
||||
})
|
||||
|
||||
// No matches.
|
||||
got = ResolveWorkspacePattern(fs, root, "nope/*")
|
||||
c.Assert(got, qt.HasLen, 0)
|
||||
}
|
||||
|
||||
func TestFilterUnwantedMounts(t *testing.T) {
|
||||
mounts := []Mount{
|
||||
{Source: "a", Target: "b", Lang: "en"},
|
||||
|
||||
@@ -403,8 +403,20 @@ type Import struct {
|
||||
Disable bool
|
||||
// File mounts.
|
||||
Mounts []Mount
|
||||
|
||||
// Controls whether npm package files (package.json/package.hugo.json) are
|
||||
// read from this module. Values: "auto" (default), "always", "never".
|
||||
// "auto" reads package files when a Hugo config file or package.hugo.json
|
||||
// is present in the module root.
|
||||
UsePackageJSON string
|
||||
}
|
||||
|
||||
const (
|
||||
UsePackageJSONAuto = "auto"
|
||||
UsePackageJSONAlways = "always"
|
||||
UsePackageJSONNever = "never"
|
||||
)
|
||||
|
||||
type Mount struct {
|
||||
// Relative path in source repo, e.g. "scss".
|
||||
Source string
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ type Module interface {
|
||||
// The version query requested in the import.
|
||||
VersionQuery() string
|
||||
|
||||
// The expected cryptographic hash of the module.
|
||||
// Checksum for path, version
|
||||
Sum() string
|
||||
|
||||
// Time version was created.
|
||||
|
||||
+540
-134
@@ -15,16 +15,20 @@ package npm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/afero"
|
||||
@@ -36,125 +40,16 @@ const (
|
||||
dependenciesKey = "dependencies"
|
||||
devDependenciesKey = "devDependencies"
|
||||
|
||||
packageJSONName = "package.json"
|
||||
|
||||
packageJSONTemplate = `{
|
||||
"name": "%s",
|
||||
"version": "%s"
|
||||
}`
|
||||
packageJSONName = "package.json"
|
||||
packageMetaJSONName = "hugo_packagemeta.json"
|
||||
)
|
||||
|
||||
func Pack(sourceFs, assetsWithDuplicatesPreservedFs afero.Fs) error {
|
||||
var b *packageBuilder
|
||||
var (
|
||||
workspacePackageJSON = filepath.Join(files.FolderPackagesHugoAutoGen, packageJSONName)
|
||||
workspacePackageMetaJSON = filepath.Join(files.FolderPackagesHugoAutoGen, packageMetaJSONName)
|
||||
)
|
||||
|
||||
// Have a package.hugo.json?
|
||||
fi, err := sourceFs.Stat(files.FilenamePackageHugoJSON)
|
||||
if err != nil {
|
||||
// Have a package.json?
|
||||
fi, err = sourceFs.Stat(packageJSONName)
|
||||
if err == nil {
|
||||
// Preserve the original in package.hugo.json.
|
||||
if err = hugio.CopyFile(sourceFs, packageJSONName, files.FilenamePackageHugoJSON); err != nil {
|
||||
return fmt.Errorf("npm pack: failed to copy package file: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Create one.
|
||||
name := "project"
|
||||
// Use the Hugo project's folder name as the default name.
|
||||
// The owner can change it later.
|
||||
rfi, err := sourceFs.Stat("")
|
||||
if err == nil {
|
||||
name = rfi.Name()
|
||||
}
|
||||
packageJSONContent := fmt.Sprintf(packageJSONTemplate, name, "0.1.0")
|
||||
if err = afero.WriteFile(sourceFs, files.FilenamePackageHugoJSON, []byte(packageJSONContent), 0o666); err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err = sourceFs.Stat(files.FilenamePackageHugoJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meta := fi.(hugofs.FileMetaInfo).Meta()
|
||||
masterFilename := meta.Filename
|
||||
f, err := meta.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm pack: failed to open package file: %w", err)
|
||||
}
|
||||
|
||||
b = newPackageBuilder(meta.ModulePath(), f)
|
||||
f.Close()
|
||||
|
||||
d, err := assetsWithDuplicatesPreservedFs.Open(files.FolderJSConfig)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fis, err := d.(fs.ReadDirFile).ReadDir(-1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm pack: failed to read assets: %w", err)
|
||||
}
|
||||
|
||||
for _, fi := range fis {
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if fi.Name() != files.FilenamePackageHugoJSON {
|
||||
continue
|
||||
}
|
||||
|
||||
meta := fi.(hugofs.FileMetaInfo).Meta()
|
||||
|
||||
if meta.Filename == masterFilename {
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := meta.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm pack: failed to open package file: %w", err)
|
||||
}
|
||||
b.Add(meta.ModulePath(), f)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
if b.Err() != nil {
|
||||
return fmt.Errorf("npm pack: failed to build: %w", b.Err())
|
||||
}
|
||||
|
||||
// Replace the dependencies in the original template with the merged set.
|
||||
b.originalPackageJSON[dependenciesKey] = b.dependencies
|
||||
b.originalPackageJSON[devDependenciesKey] = b.devDependencies
|
||||
var commentsm map[string]any
|
||||
comments, found := b.originalPackageJSON["comments"]
|
||||
if found {
|
||||
commentsm = hmaps.ToStringMap(comments)
|
||||
} else {
|
||||
commentsm = make(map[string]any)
|
||||
}
|
||||
commentsm[dependenciesKey] = b.dependenciesComments
|
||||
commentsm[devDependenciesKey] = b.devDependenciesComments
|
||||
b.originalPackageJSON["comments"] = commentsm
|
||||
|
||||
// Write it out to the project package.json
|
||||
packageJSONData := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(packageJSONData)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", strings.Repeat(" ", 2))
|
||||
if err := encoder.Encode(b.originalPackageJSON); err != nil {
|
||||
return fmt.Errorf("npm pack: failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
if err := afero.WriteFile(sourceFs, packageJSONName, packageJSONData.Bytes(), 0o666); err != nil {
|
||||
return fmt.Errorf("npm pack: failed to write package.json: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPackageBuilder(source string, first io.Reader) *packageBuilder {
|
||||
func Pack(sourceFs, assetsWithDuplicatesPreservedFs afero.Fs, mods modules.Modules) error {
|
||||
b := &packageBuilder{
|
||||
devDependencies: make(map[string]any),
|
||||
devDependenciesComments: make(map[string]any),
|
||||
@@ -162,23 +57,362 @@ func newPackageBuilder(source string, first io.Reader) *packageBuilder {
|
||||
dependenciesComments: make(map[string]any),
|
||||
}
|
||||
|
||||
m := b.unmarshal(first)
|
||||
if b.err != nil {
|
||||
return b
|
||||
workspacePath := filepath.ToSlash(files.FolderPackagesHugoAutoGen)
|
||||
|
||||
// skip modules that shouldn't have their package files processed, either because they are the project module (handled separately)
|
||||
// or because their UsePackageJSON setting disables it.
|
||||
skipPackageJSON := buildSkipPackageJSON(mods)
|
||||
|
||||
// 1. Read project deps: prefer package.hugo.json, fall back to package.json.
|
||||
var rootPkg map[string]any
|
||||
rootData, err := afero.ReadFile(sourceFs, packageJSONName)
|
||||
if err == nil {
|
||||
rootPkg = b.unmarshal(bytes.NewReader(rootData))
|
||||
if b.err != nil {
|
||||
return fmt.Errorf("npm pack: failed to parse package.json: %w", b.err)
|
||||
}
|
||||
}
|
||||
|
||||
b.addm(source, m)
|
||||
b.originalPackageJSON = m
|
||||
// Workspaces source: prefer package.hugo.json, fall back to package.json.
|
||||
var workspacesSource map[string]any
|
||||
hugoData, hugoErr := afero.ReadFile(sourceFs, files.FilenamePackageHugoJSON)
|
||||
if hugoErr == nil {
|
||||
hugoPkg := b.unmarshal(bytes.NewReader(hugoData))
|
||||
if b.err != nil {
|
||||
return fmt.Errorf("npm pack: failed to parse %s: %w", files.FilenamePackageHugoJSON, b.err)
|
||||
}
|
||||
b.addm("project", hugoPkg)
|
||||
workspacesSource = hugoPkg
|
||||
} else if rootPkg != nil {
|
||||
b.addm("project", rootPkg)
|
||||
workspacesSource = rootPkg
|
||||
}
|
||||
|
||||
return b
|
||||
// 2. Read deps from referenced workspaces (always from package.json).
|
||||
for _, wsDir := range resolveProjectWorkspaces(sourceFs, workspacesSource, workspacePath) {
|
||||
wsFile := filepath.Join(wsDir, packageJSONName)
|
||||
wsData, err := afero.ReadFile(sourceFs, wsFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
wsm := b.unmarshal(bytes.NewReader(wsData))
|
||||
if b.err != nil {
|
||||
return fmt.Errorf("npm pack: failed to parse %s: %w", wsFile, b.err)
|
||||
}
|
||||
b.addm("project", wsm)
|
||||
}
|
||||
|
||||
// 3. Walk _jsconfig for module deps.
|
||||
// We use hugofs.Walkway (which uses ReadDir) instead of afero.Walk because
|
||||
// afero.Walk uses Readdirnames+Stat, which loses the per-module identity
|
||||
// when multiple modules mount a file (e.g. package.json) to the same virtual path.
|
||||
// Note that the order of files here is in the order of importance.
|
||||
type pkgEntry struct {
|
||||
info hugofs.FileMetaInfo
|
||||
isHugoJSON bool
|
||||
isRootLevel bool
|
||||
}
|
||||
var entries []pkgEntry
|
||||
modulesWithHugoJSON := make(map[string]bool)
|
||||
|
||||
w := hugofs.NewWalkway(hugofs.WalkwayConfig{
|
||||
Fs: assetsWithDuplicatesPreservedFs,
|
||||
Root: files.FolderJSConfig,
|
||||
WalkFn: func(ctx context.Context, path string, info hugofs.FileMetaInfo) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
isPackageJSON := info.Name() == files.FilenamePackageJSON
|
||||
isHugoJSON := info.Name() == files.FilenamePackageHugoJSON
|
||||
|
||||
if !isPackageJSON && !isHugoJSON {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := info.Meta()
|
||||
if skipPackageJSON[m.ModulePath()] {
|
||||
return nil
|
||||
}
|
||||
|
||||
// package.hugo.json is only valid at module roots, not inside workspaces.
|
||||
isRootLevel := filepath.Dir(path) == files.FolderJSConfig
|
||||
if isHugoJSON && !isRootLevel {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isHugoJSON {
|
||||
modulesWithHugoJSON[m.ModulePath()] = true
|
||||
}
|
||||
|
||||
entries = append(entries, pkgEntry{info: info, isHugoJSON: isHugoJSON, isRootLevel: isRootLevel})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err := w.Walk(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process collected entries: for each module, prefer package.hugo.json
|
||||
// over package.json at the root level. Workspace package.json files are always processed.
|
||||
for _, e := range entries {
|
||||
m := e.info.Meta()
|
||||
// Skip root-level package.json if this module has package.hugo.json.
|
||||
if !e.isHugoJSON && e.isRootLevel && modulesWithHugoJSON[m.ModulePath()] {
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := m.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm pack: failed to open package file: %w", err)
|
||||
}
|
||||
b.Add(m.ModulePath(), f)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
if b.Err() != nil {
|
||||
return fmt.Errorf("npm pack: failed to build: %w", b.Err())
|
||||
}
|
||||
|
||||
// 4. Build the autogenerated workspace package.json.
|
||||
// Exclude deps already defined by the project itself — they don't
|
||||
// need to be duplicated in the workspace and it simplifies maintenance.
|
||||
moduleDeps := make(map[string]any)
|
||||
moduleDepsComments := make(map[string]any)
|
||||
for k, v := range b.dependencies {
|
||||
if b.dependenciesComments[k] != "project" {
|
||||
moduleDeps[k] = v
|
||||
moduleDepsComments[k] = b.dependenciesComments[k]
|
||||
}
|
||||
}
|
||||
moduleDevDeps := make(map[string]any)
|
||||
moduleDevDepsComments := make(map[string]any)
|
||||
for k, v := range b.devDependencies {
|
||||
if b.devDependenciesComments[k] != "project" {
|
||||
moduleDevDeps[k] = v
|
||||
moduleDevDepsComments[k] = b.devDependenciesComments[k]
|
||||
}
|
||||
}
|
||||
|
||||
name := "project"
|
||||
rfi, err := sourceFs.Stat("")
|
||||
if err == nil {
|
||||
name = rfi.Name()
|
||||
}
|
||||
|
||||
autoGenPkg := map[string]any{
|
||||
"name": name,
|
||||
"version": "0.1.0",
|
||||
dependenciesKey: moduleDeps,
|
||||
devDependenciesKey: moduleDevDeps,
|
||||
}
|
||||
|
||||
metaFile := packageMeta{
|
||||
Sum: PackageFilesSum(sourceFs, mods),
|
||||
DependencySources: dependencySources{
|
||||
Dependencies: moduleDepsComments,
|
||||
DevDependencies: moduleDevDepsComments,
|
||||
},
|
||||
}
|
||||
|
||||
if err := sourceFs.MkdirAll(files.FolderPackagesHugoAutoGen, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeJSON(sourceFs, workspacePackageJSON, autoGenPkg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeJSON(sourceFs, workspacePackageMetaJSON, metaFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Ensure root package.json references the workspace.
|
||||
return ensureWorkspaceRef(sourceFs, workspacePath)
|
||||
}
|
||||
|
||||
// ensureWorkspaceRef adds workspacePath to the "workspaces" array in root
|
||||
// package.json with minimal formatting changes.
|
||||
func ensureWorkspaceRef(fsys afero.Fs, workspacePath string) error {
|
||||
data, err := afero.ReadFile(fsys, packageJSONName)
|
||||
if err != nil {
|
||||
content := fmt.Sprintf("{\n \"workspaces\": [\n %q\n ]\n}\n", workspacePath)
|
||||
return afero.WriteFile(fsys, packageJSONName, []byte(content), 0o666)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
|
||||
}
|
||||
|
||||
// Parse to check if already present.
|
||||
var pkg map[string]any
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
return fmt.Errorf("npm pack: failed to parse package.json: %w", err)
|
||||
}
|
||||
|
||||
indent := detectIndent(data)
|
||||
quoted := fmt.Sprintf("%q", workspacePath)
|
||||
|
||||
wsVal, hasWS := pkg["workspaces"]
|
||||
|
||||
switch v := wsVal.(type) {
|
||||
case []interface{}:
|
||||
// Array form: ["pkg-a", "pkg-b", ...]
|
||||
if containsString(toStringSlice(v), workspacePath) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return afero.WriteFile(fsys, packageJSONName, data, 0o666)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Fall through to byte-based insertion into the existing array below.
|
||||
case map[string]interface{}:
|
||||
// Object form: { "workspaces": { "packages": [...] } }
|
||||
packagesVal, ok := v["packages"]
|
||||
if !ok {
|
||||
return fmt.Errorf("npm pack: unsupported workspaces object; missing \"packages\" field")
|
||||
}
|
||||
packagesSlice := toStringSlice(packagesVal)
|
||||
if containsString(packagesSlice, workspacePath) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return afero.WriteFile(fsys, packageJSONName, data, 0o666)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append the new workspace path to the packages slice.
|
||||
newPkgs := make([]interface{}, 0, len(packagesSlice)+1)
|
||||
for _, s := range packagesSlice {
|
||||
newPkgs = append(newPkgs, s)
|
||||
}
|
||||
newPkgs = append(newPkgs, workspacePath)
|
||||
v["packages"] = newPkgs
|
||||
|
||||
updated, err := json.MarshalIndent(pkg, "", indent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("npm pack: failed to marshal package.json with updated workspaces: %w", err)
|
||||
}
|
||||
updated = append(updated, '\n')
|
||||
return afero.WriteFile(fsys, packageJSONName, updated, 0o666)
|
||||
case nil:
|
||||
// Treat explicit null as "no workspaces"; handled below as missing key.
|
||||
default:
|
||||
return fmt.Errorf("npm pack: unsupported workspaces type %T", v)
|
||||
}
|
||||
|
||||
// Try adding to existing workspaces array when present.
|
||||
if hasWS && wsVal != nil {
|
||||
if wsIdx := bytes.Index(data, []byte(`"workspaces"`)); wsIdx >= 0 {
|
||||
rest := data[wsIdx:]
|
||||
if bracketOpen := bytes.IndexByte(rest, '['); bracketOpen >= 0 {
|
||||
if bracketClose := bytes.IndexByte(rest[bracketOpen:], ']'); bracketClose >= 0 {
|
||||
pos := wsIdx + bracketOpen + bracketClose
|
||||
arrayContent := bytes.TrimSpace(data[wsIdx+bracketOpen+1 : pos])
|
||||
var insertion string
|
||||
if len(arrayContent) == 0 {
|
||||
insertion = "\n" + indent + indent + quoted + "\n" + indent
|
||||
} else {
|
||||
insertion = ",\n" + indent + indent + quoted + "\n" + indent
|
||||
}
|
||||
result := make([]byte, 0, len(data)+len(insertion))
|
||||
result = append(result, data[:pos]...)
|
||||
result = append(result, insertion...)
|
||||
result = append(result, data[pos:]...)
|
||||
return afero.WriteFile(fsys, packageJSONName, result, 0o666)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We know a workspaces array exists but could not locate it reliably in the raw data.
|
||||
return fmt.Errorf("npm pack: could not locate existing workspaces array for insertion")
|
||||
}
|
||||
|
||||
// No workspaces key — add before closing brace.
|
||||
lastBrace := bytes.LastIndexByte(data, '}')
|
||||
if lastBrace < 0 {
|
||||
return fmt.Errorf("npm pack: malformed package.json")
|
||||
}
|
||||
|
||||
before := bytes.TrimRight(data[:lastBrace], " \t\n\r")
|
||||
needComma := len(before) > 0 && before[len(before)-1] != '{' && before[len(before)-1] != ','
|
||||
|
||||
var insertion string
|
||||
if needComma {
|
||||
insertion = ",\n" + indent + `"workspaces": [` + "\n" + indent + indent + quoted + "\n" + indent + "]\n"
|
||||
} else {
|
||||
insertion = indent + `"workspaces": [` + "\n" + indent + indent + quoted + "\n" + indent + "]\n"
|
||||
}
|
||||
|
||||
result := make([]byte, 0, len(data)+len(insertion))
|
||||
result = append(result, before...)
|
||||
result = append(result, insertion...)
|
||||
result = append(result, data[lastBrace:]...)
|
||||
return afero.WriteFile(fsys, packageJSONName, result, 0o666)
|
||||
}
|
||||
|
||||
func detectIndent(data []byte) string {
|
||||
for _, line := range bytes.Split(data, []byte("\n")) {
|
||||
trimmed := bytes.TrimLeft(line, " \t")
|
||||
if len(trimmed) < len(line) && len(trimmed) > 0 && trimmed[0] == '"' {
|
||||
return string(line[:len(line)-len(trimmed)])
|
||||
}
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
func writeJSON(fs afero.Fs, filename string, v any) error {
|
||||
buf := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", strings.Repeat(" ", 2))
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
return fmt.Errorf("npm pack: failed to marshal JSON: %w", err)
|
||||
}
|
||||
return afero.WriteFile(fs, filename, buf.Bytes(), 0o666)
|
||||
}
|
||||
|
||||
func toStringSlice(v any) []string {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if s, ok := v.([]any); ok {
|
||||
var out []string
|
||||
for _, item := range s {
|
||||
if str, ok := item.(string); ok {
|
||||
out = append(out, str)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsString(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveProjectWorkspaces resolves workspace patterns from the project's
|
||||
// package source, skipping the hugoautogen workspace.
|
||||
func resolveProjectWorkspaces(sourceFs afero.Fs, workspacesSource map[string]any, skipPath string) []string {
|
||||
if workspacesSource == nil {
|
||||
return nil
|
||||
}
|
||||
var dirs []string
|
||||
for _, ws := range toStringSlice(workspacesSource["workspaces"]) {
|
||||
for _, wsDir := range modules.ResolveWorkspacePattern(sourceFs, "", ws) {
|
||||
if filepath.ToSlash(wsDir) != skipPath {
|
||||
dirs = append(dirs, wsDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
type packageBuilder struct {
|
||||
err error
|
||||
|
||||
// The original package.hugo.json.
|
||||
originalPackageJSON map[string]any
|
||||
|
||||
devDependencies map[string]any
|
||||
devDependenciesComments map[string]any
|
||||
dependencies map[string]any
|
||||
@@ -205,13 +439,9 @@ func (b *packageBuilder) addm(source string, m map[string]any) {
|
||||
source = "project"
|
||||
}
|
||||
|
||||
// The version selection is currently very simple.
|
||||
// We may consider minimal version selection or something
|
||||
// after testing this out.
|
||||
//
|
||||
// But for now, the first version string for a given dependency wins.
|
||||
// These packages will be added by order of import (project, module1, module2...),
|
||||
// so that should at least give the project control over the situation.
|
||||
// First version for a given dependency wins.
|
||||
// Packages added by order of import (project, module1, module2...),
|
||||
// so the project has control over versions.
|
||||
if devDeps, found := m[devDependenciesKey]; found {
|
||||
mm := hmaps.ToStringMapString(devDeps)
|
||||
for k, v := range mm {
|
||||
@@ -245,3 +475,179 @@ func (b *packageBuilder) unmarshal(r io.Reader) map[string]any {
|
||||
func (b *packageBuilder) Err() error {
|
||||
return b.err
|
||||
}
|
||||
|
||||
// buildSkipPackageJSON determines which modules should NOT have their package
|
||||
// files processed. The project module is always skipped (handled separately).
|
||||
// For other modules, the behavior is controlled by the UsePackageJSON import
|
||||
// setting: "auto" (default) reads package files when a Hugo config file or
|
||||
// package.hugo.json is present; "always" always reads; "never" never reads.
|
||||
func buildSkipPackageJSON(mods modules.Modules) map[string]bool {
|
||||
skip := make(map[string]bool)
|
||||
for _, m := range mods {
|
||||
if m.Owner() == nil {
|
||||
skip[m.Path()] = true
|
||||
continue
|
||||
}
|
||||
if !usePackageJSON(m) {
|
||||
skip[m.Path()] = true
|
||||
}
|
||||
}
|
||||
return skip
|
||||
}
|
||||
|
||||
// usePackageJSON checks the import config for this module and applies the
|
||||
// UsePackageJSON setting. For "auto", it checks for Hugo config files or
|
||||
// package.hugo.json in the module root.
|
||||
func usePackageJSON(m modules.Module) bool {
|
||||
setting := findImportSetting(m)
|
||||
switch setting {
|
||||
case modules.UsePackageJSONAlways:
|
||||
return true
|
||||
case modules.UsePackageJSONNever:
|
||||
return false
|
||||
default:
|
||||
// "auto": use if Hugo config file or package.hugo.json is present.
|
||||
if len(m.ConfigFilenames()) > 0 {
|
||||
return true
|
||||
}
|
||||
if m.Dir() != "" {
|
||||
if _, err := os.Stat(filepath.Join(m.Dir(), files.FilenamePackageHugoJSON)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func findImportSetting(m modules.Module) string {
|
||||
if m.Owner() == nil {
|
||||
return modules.UsePackageJSONAuto
|
||||
}
|
||||
for _, imp := range m.Owner().Config().Imports {
|
||||
if imp.Path == m.Path() {
|
||||
if imp.UsePackageJSON != "" {
|
||||
return imp.UsePackageJSON
|
||||
}
|
||||
return modules.UsePackageJSONAuto
|
||||
}
|
||||
}
|
||||
return modules.UsePackageJSONAuto
|
||||
}
|
||||
|
||||
type packageMeta struct {
|
||||
// Sum is a hash of all package files that feed into the npm pack output.
|
||||
Sum string `json:"sum"`
|
||||
|
||||
DependencySources dependencySources `json:"dependencySources"`
|
||||
}
|
||||
|
||||
type dependencySources struct {
|
||||
Dependencies map[string]any `json:"dependencies"`
|
||||
DevDependencies map[string]any `json:"devDependencies"`
|
||||
}
|
||||
|
||||
// PackageFilesSum hashes the package files that Pack would use,
|
||||
// using the same file selection logic as Pack.
|
||||
func PackageFilesSum(sourceFs afero.Fs, mods modules.Modules) string {
|
||||
h := hashing.XxHasher()
|
||||
defer h.Close()
|
||||
|
||||
var w io.Writer = h
|
||||
if runtime.GOOS == "windows" {
|
||||
w = &crlfReplacer{w: h}
|
||||
}
|
||||
|
||||
copyFile := func(fsys afero.Fs, name string) {
|
||||
f, err := fsys.Open(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(w, f)
|
||||
}
|
||||
copyOsFile := func(name string) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(w, f)
|
||||
}
|
||||
|
||||
// Project level: prefer package.hugo.json, fall back to package.json.
|
||||
// We need to parse whichever file we pick to discover workspaces.
|
||||
var workspacesSource map[string]any
|
||||
if data, err := afero.ReadFile(sourceFs, files.FilenamePackageHugoJSON); err == nil {
|
||||
w.Write(data)
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(data, &m); err == nil {
|
||||
workspacesSource = m
|
||||
}
|
||||
} else if data, err := afero.ReadFile(sourceFs, packageJSONName); err == nil {
|
||||
w.Write(data)
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(data, &m); err == nil {
|
||||
workspacesSource = m
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace package.json files (skipping hugoautogen).
|
||||
workspacePath := filepath.ToSlash(files.FolderPackagesHugoAutoGen)
|
||||
for _, wsDir := range resolveProjectWorkspaces(sourceFs, workspacesSource, workspacePath) {
|
||||
copyFile(sourceFs, filepath.Join(wsDir, packageJSONName))
|
||||
}
|
||||
|
||||
// Module package files: prefer package.hugo.json, fall back to package.json.
|
||||
skip := buildSkipPackageJSON(mods)
|
||||
modulesWithHugoJSON := make(map[string]bool)
|
||||
for _, m := range mods {
|
||||
if skip[m.Path()] || m.Dir() == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(m.Dir(), files.FilenamePackageHugoJSON)); err == nil {
|
||||
modulesWithHugoJSON[m.Path()] = true
|
||||
}
|
||||
}
|
||||
for _, m := range mods {
|
||||
if skip[m.Path()] || m.Dir() == "" {
|
||||
continue
|
||||
}
|
||||
if modulesWithHugoJSON[m.Path()] {
|
||||
copyOsFile(filepath.Join(m.Dir(), files.FilenamePackageHugoJSON))
|
||||
} else {
|
||||
copyOsFile(filepath.Join(m.Dir(), packageJSONName))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum64())
|
||||
}
|
||||
|
||||
// crlfReplacer wraps a writer and strips \r bytes.
|
||||
type crlfReplacer struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (c *crlfReplacer) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
_, err := c.w.Write(bytes.ReplaceAll(p, []byte("\r\n"), []byte("\n")))
|
||||
return n, err
|
||||
}
|
||||
|
||||
// NpmPackNeedsUpdate checks if the npm pack output is stale by comparing
|
||||
// the stored hash against the current package files.
|
||||
func NpmPackNeedsUpdate(sourceFs afero.Fs, mods modules.Modules) bool {
|
||||
data, err := afero.ReadFile(sourceFs, workspacePackageMetaJSON)
|
||||
if err != nil {
|
||||
// No meta file means npm pack hasn't been run yet.
|
||||
return false
|
||||
}
|
||||
|
||||
// We only need the sum for this check.
|
||||
meta := struct {
|
||||
Sum string `json:"sum"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &meta); err != nil || meta.Sum == "" {
|
||||
return true
|
||||
}
|
||||
return meta.Sum != PackageFilesSum(sourceFs, mods)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 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 gxoverning permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package npm_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
)
|
||||
|
||||
func getPackageBuilderTestFiles() string {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- package.json --
|
||||
{
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
-- packages/hugoautogen/package.json --
|
||||
-- packages/a/package.json --
|
||||
PACKAGE_CONTENT
|
||||
-- packages/b/package.json --
|
||||
PACKAGE_CONTENT
|
||||
-- packages/c/package.json --
|
||||
PACKAGE_CONTENT
|
||||
-- packages/d/package.json --
|
||||
PACKAGE_CONTENT
|
||||
-- packages/e/package.json --
|
||||
PACKAGE_CONTENT
|
||||
`
|
||||
packageContent := `{
|
||||
"name": "foo",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"react-dom": "1.1.1",
|
||||
"tailwindcss": "1.2.0",
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"postcss-cli": "7.1.0",
|
||||
"tailwindcss": "1.2.0",
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.5"
|
||||
}
|
||||
}`
|
||||
files = strings.ReplaceAll(files, "PACKAGE_CONTENT", packageContent)
|
||||
return files
|
||||
}
|
||||
|
||||
func TestPackageBuilder(t *testing.T) {
|
||||
files := getPackageBuilderTestFiles()
|
||||
b := hugolib.Test(t, files)
|
||||
fs := b.H.Fs.WorkingDirReadOnly
|
||||
|
||||
sum := npm.PackageFilesSum(fs, b.H.AllModules())
|
||||
b.Assert(sum, qt.Equals, "ce880d142ad9a16a")
|
||||
}
|
||||
|
||||
func BenchmarkPackageFilesSum(b *testing.B) {
|
||||
files := getPackageBuilderTestFiles()
|
||||
bb := hugolib.Test(b, files)
|
||||
fs := bb.H.Fs.WorkingDirReadOnly
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
sum := npm.PackageFilesSum(fs, modules.Modules{})
|
||||
bb.Assert(sum, qt.Equals, "ce880d142ad9a16a")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 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.
|
||||
@@ -8,7 +8,7 @@
|
||||
// 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
|
||||
// See the License for the specific language gxoverning permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package npm
|
||||
@@ -43,7 +43,13 @@ const templ = `{
|
||||
func TestPackageBuilder(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
b := newPackageBuilder("", strings.NewReader(templ))
|
||||
b := &packageBuilder{
|
||||
devDependencies: make(map[string]any),
|
||||
devDependenciesComments: make(map[string]any),
|
||||
dependencies: make(map[string]any),
|
||||
dependenciesComments: make(map[string]any),
|
||||
}
|
||||
b.Add("", strings.NewReader(templ))
|
||||
c.Assert(b.Err(), qt.IsNil)
|
||||
|
||||
b.Add("mymod", strings.NewReader(`{
|
||||
|
||||
@@ -1,41 +1,218 @@
|
||||
# Test mod npm.
|
||||
|
||||
dostounix golden/package.json
|
||||
# Test mod npm pack.
|
||||
|
||||
dostounix golden1/package.json
|
||||
dostounix golden1/packages/hugoautogen/package.json
|
||||
dostounix golden1/packages/hugoautogen/hugo_packagemeta.json
|
||||
dostounix golden2/packages/hugoautogen/package.json
|
||||
dostounix golden3/packages/hugoautogen/package.json
|
||||
dostounix golden4/packages/hugoautogen/package.json
|
||||
dostounix golden5/packages/hugoautogen/package.json
|
||||
|
||||
hugo mod npm pack
|
||||
cmp package.json golden/package.json
|
||||
cmp packages/hugoautogen/package.json golden1/packages/hugoautogen/package.json
|
||||
cmp package.json golden1/package.json
|
||||
cmp packages/hugoautogen/hugo_packagemeta.json golden1/packages/hugoautogen/hugo_packagemeta.json
|
||||
|
||||
# c module commits:
|
||||
# 324e3adae608ff8b6a52b4630975ffe652859f27: add is-odd
|
||||
# 40c59ae8595d8ff3f56b87fcf4ba3325649a9a52: replace is-odd with is-even
|
||||
# 7dd9b2dc7f505508adbb3fdea36c308aad0de3ec: add package.hugo.json with is-number
|
||||
# bf55e8bb813a7f6cbe6f212bd7e1dd4fb44c79df: update README.md only
|
||||
# cc5188f671e1852f470df9fed52c7f93d1cab476: Add myworkspace with my-prime (in package.hugo.json)
|
||||
replace hugo.toml "324e3adae608ff8b6a52b4630975ffe652859f27" "40c59ae8595d8ff3f56b87fcf4ba3325649a9a52"
|
||||
|
||||
hugo mod graph
|
||||
stderr 'WARN npm dependencies are out of sync'
|
||||
|
||||
hugo mod npm pack
|
||||
cmp packages/hugoautogen/package.json golden2/packages/hugoautogen/package.json
|
||||
|
||||
replace hugo.toml "40c59ae8595d8ff3f56b87fcf4ba3325649a9a52" "7dd9b2dc7f505508adbb3fdea36c308aad0de3ec"
|
||||
hugo mod npm pack
|
||||
cmp packages/hugoautogen/package.json golden3/packages/hugoautogen/package.json
|
||||
|
||||
# This commit only updates README.md, no npm package updates.
|
||||
replace hugo.toml "7dd9b2dc7f505508adbb3fdea36c308aad0de3ec" "bf55e8bb813a7f6cbe6f212bd7e1dd4fb44c79df"
|
||||
hugo mod graph
|
||||
! stderr 'WARN'
|
||||
|
||||
# Add myworkspace with my-prime (in package.hugo.json)
|
||||
replace hugo.toml "bf55e8bb813a7f6cbe6f212bd7e1dd4fb44c79df" "cc5188f671e1852f470df9fed52c7f93d1cab476"
|
||||
hugo mod npm pack
|
||||
cmp packages/hugoautogen/package.json golden4/packages/hugoautogen/package.json
|
||||
|
||||
# usePackageJSON="auto" => usePackageJSON="never" in hugo.toml will effectively disable c's npm deps.
|
||||
replace hugo.toml "auto" "never"
|
||||
hugo mod graph
|
||||
# dependencies are out of sync
|
||||
stderr 'WARN'
|
||||
hugo mod npm pack
|
||||
cmp packages/hugoautogen/package.json golden5/packages/hugoautogen/package.json
|
||||
# Turn it back on using "always"
|
||||
replace hugo.toml "never" "always"
|
||||
hugo mod graph
|
||||
stderr 'WARN'
|
||||
hugo mod npm pack
|
||||
hugo mod graph
|
||||
! stderr 'WARN'
|
||||
cmp packages/hugoautogen/package.json golden4/packages/hugoautogen/package.json
|
||||
|
||||
# Now test vendoring..
|
||||
hugo mod vendor
|
||||
# a ab c@cc5188f671e1852f470df9fed52c7f93d1cab476 d
|
||||
ls _vendor/github.com/gohugoio/hugoTestModsNPMNested/a
|
||||
stdout 'package.json'
|
||||
ls _vendor/github.com/gohugoio/hugoTestModsNPMNested/a/packages/aws1
|
||||
stdout 'package.json'
|
||||
|
||||
ls _vendor/github.com/gohugoio/hugoTestModsNPMNested/c@cc5188f671e1852f470df9fed52c7f93d1cab476
|
||||
stdout 'package.hugo.json'
|
||||
# package.json is not needed when package.hugo.json exists.
|
||||
! stdout 'package.json'
|
||||
! exists _vendor/github.com/gohugoio/hugoTestModsNPMNested/ab/packages/hugoautogen
|
||||
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/"
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path="github.com/gohugoio/hugoTestModule2"
|
||||
|
||||
|
||||
-- golden/package.json --
|
||||
path="github.com/gohugoio/hugoTestModsNPMNested/a"
|
||||
[[module.imports]]
|
||||
path="github.com/gohugoio/hugoTestModsNPMNested/c"
|
||||
version="324e3adae608ff8b6a52b4630975ffe652859f27"
|
||||
usePackageJSON="auto"
|
||||
-- package.json --
|
||||
{
|
||||
"comments": {
|
||||
"workspaces": [
|
||||
"packages/hugoautogen"
|
||||
],
|
||||
"name": "npmpack",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"dedupe": "4.0.3"
|
||||
}
|
||||
}
|
||||
-- golden1/packages/hugoautogen/hugo_packagemeta.json --
|
||||
{
|
||||
"sum": "259f33c1d93dcc01",
|
||||
"dependencySources": {
|
||||
"dependencies": {
|
||||
"react-dom": "github.com/gohugoio/hugoTestModule2"
|
||||
"count-days-in-month": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"fast-cartesian": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"is-odd": "github.com/gohugoio/hugoTestModsNPMNested/c",
|
||||
"is-sorted": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"once": "github.com/gohugoio/hugoTestModsNPMNested/d",
|
||||
"pluralize": "github.com/gohugoio/hugoTestModsNPMNested/ab",
|
||||
"to-space-case": "github.com/gohugoio/hugoTestModsNPMNested/a"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "github.com/gohugoio/hugoTestModule2",
|
||||
"@babel/core": "github.com/gohugoio/hugoTestModule2",
|
||||
"@babel/preset-env": "github.com/gohugoio/hugoTestModule2",
|
||||
"postcss-cli": "github.com/gohugoio/hugoTestModule2",
|
||||
"tailwindcss": "github.com/gohugoio/hugoTestModule2"
|
||||
"decamelize": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"strip-ansi": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"to-pascal-case": "github.com/gohugoio/hugoTestModsNPMNested/a"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
-- golden1/packages/hugoautogen/package.json --
|
||||
{
|
||||
"dependencies": {
|
||||
"react-dom": "^16.13.1"
|
||||
"count-days-in-month": "^1.0.0",
|
||||
"fast-cartesian": "9.0.0",
|
||||
"is-odd": "3.0.0",
|
||||
"is-sorted": "1.0.4",
|
||||
"once": "1.4.0",
|
||||
"pluralize": "8.0.0",
|
||||
"to-space-case": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.5",
|
||||
"postcss-cli": "7.1.0",
|
||||
"tailwindcss": "1.2.0"
|
||||
"decamelize": "6.0.0",
|
||||
"strip-ansi": "7.2.0",
|
||||
"to-pascal-case": "1.0.0"
|
||||
},
|
||||
"name": "script-mod_npm",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
-- golden1/package.json --
|
||||
{
|
||||
"workspaces": [
|
||||
"packages/hugoautogen"
|
||||
],
|
||||
"name": "npmpack",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"dedupe": "4.0.3"
|
||||
}
|
||||
}
|
||||
-- golden2/packages/hugoautogen/package.json --
|
||||
{
|
||||
"dependencies": {
|
||||
"count-days-in-month": "^1.0.0",
|
||||
"fast-cartesian": "9.0.0",
|
||||
"is-even": "1.0.0",
|
||||
"is-sorted": "1.0.4",
|
||||
"once": "1.4.0",
|
||||
"pluralize": "8.0.0",
|
||||
"to-space-case": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"decamelize": "6.0.0",
|
||||
"strip-ansi": "7.2.0",
|
||||
"to-pascal-case": "1.0.0"
|
||||
},
|
||||
"name": "script-mod_npm",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
-- golden3/packages/hugoautogen/package.json --
|
||||
{
|
||||
"dependencies": {
|
||||
"count-days-in-month": "^1.0.0",
|
||||
"fast-cartesian": "9.0.0",
|
||||
"is-number": "7.0.0",
|
||||
"is-sorted": "1.0.4",
|
||||
"once": "1.4.0",
|
||||
"pluralize": "8.0.0",
|
||||
"to-space-case": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"decamelize": "6.0.0",
|
||||
"strip-ansi": "7.2.0",
|
||||
"to-pascal-case": "1.0.0"
|
||||
},
|
||||
"name": "script-mod_npm",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
-- golden4/packages/hugoautogen/package.json --
|
||||
{
|
||||
"dependencies": {
|
||||
"count-days-in-month": "^1.0.0",
|
||||
"fast-cartesian": "9.0.0",
|
||||
"is-number": "7.0.0",
|
||||
"is-sorted": "1.0.4",
|
||||
"my-prime": "^1.0.1",
|
||||
"once": "1.4.0",
|
||||
"pluralize": "8.0.0",
|
||||
"to-space-case": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"decamelize": "6.0.0",
|
||||
"strip-ansi": "7.2.0",
|
||||
"to-pascal-case": "1.0.0"
|
||||
},
|
||||
"name": "script-mod_npm",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
-- golden5/packages/hugoautogen/package.json --
|
||||
{
|
||||
"dependencies": {
|
||||
"count-days-in-month": "^1.0.0",
|
||||
"fast-cartesian": "9.0.0",
|
||||
"is-sorted": "1.0.4",
|
||||
"once": "1.4.0",
|
||||
"pluralize": "8.0.0",
|
||||
"to-space-case": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"decamelize": "6.0.0",
|
||||
"strip-ansi": "7.2.0",
|
||||
"to-pascal-case": "1.0.0"
|
||||
},
|
||||
"name": "script-mod_npm",
|
||||
"version": "0.1.0"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Test mod npm pack with shuffled modules.
|
||||
|
||||
dostounix golden1/package.json
|
||||
dostounix golden1/packages/hugoautogen/package.json
|
||||
dostounix golden1/packages/hugoautogen/hugo_packagemeta.json
|
||||
|
||||
hugo mod npm pack
|
||||
cmp packages/hugoautogen/package.json golden1/packages/hugoautogen/package.json
|
||||
cmp package.json golden1/package.json
|
||||
cmp packages/hugoautogen/hugo_packagemeta.json golden1/packages/hugoautogen/hugo_packagemeta.json
|
||||
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/"
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path="github.com/gohugoio/hugoTestModsNPMNested/d"
|
||||
[[module.imports]]
|
||||
path="github.com/gohugoio/hugoTestModsNPMNested/a"
|
||||
|
||||
-- go.mod --
|
||||
module github.com/gohugoio/hugoTestModule
|
||||
go 1.20
|
||||
|
||||
-- golden1/package.json --
|
||||
{
|
||||
"workspaces": [
|
||||
"packages/hugoautogen"
|
||||
]
|
||||
}
|
||||
-- golden1/packages/hugoautogen/package.json --
|
||||
{
|
||||
"dependencies": {
|
||||
"count-days-in-month": "^1.0.0",
|
||||
"dedupe": "4.0.1",
|
||||
"fast-cartesian": "9.0.0",
|
||||
"is-sorted": "1.0.2",
|
||||
"once": "1.4.0",
|
||||
"pluralize": "8.0.0",
|
||||
"to-space-case": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"decamelize": "6.0.0",
|
||||
"strip-ansi": "7.0.0",
|
||||
"to-pascal-case": "1.0.0"
|
||||
},
|
||||
"name": "script-mod_npm__moduleorder",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
-- golden1/packages/hugoautogen/hugo_packagemeta.json --
|
||||
{
|
||||
"sum": "dd3590c300b0bebb",
|
||||
"dependencySources": {
|
||||
"dependencies": {
|
||||
"count-days-in-month": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"dedupe": "github.com/gohugoio/hugoTestModsNPMNested/ab",
|
||||
"fast-cartesian": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"is-sorted": "github.com/gohugoio/hugoTestModsNPMNested/d",
|
||||
"once": "github.com/gohugoio/hugoTestModsNPMNested/d",
|
||||
"pluralize": "github.com/gohugoio/hugoTestModsNPMNested/ab",
|
||||
"to-space-case": "github.com/gohugoio/hugoTestModsNPMNested/a"
|
||||
},
|
||||
"devDependencies": {
|
||||
"decamelize": "github.com/gohugoio/hugoTestModsNPMNested/a",
|
||||
"strip-ansi": "github.com/gohugoio/hugoTestModsNPMNested/ab",
|
||||
"to-pascal-case": "github.com/gohugoio/hugoTestModsNPMNested/a"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
# Test mod npm.
|
||||
# Test mod npm with existing package.json.
|
||||
|
||||
# See https://github.com/gohugoio/hugo/issues/13465
|
||||
[windows] skip
|
||||
|
||||
dostounix golden/packages/hugoautogen/package.json
|
||||
dostounix golden/package.json
|
||||
|
||||
hugo mod npm pack
|
||||
cmp packages/hugoautogen/package.json golden/packages/hugoautogen/package.json
|
||||
cmp package.json golden/package.json
|
||||
exists packages/hugoautogen/hugo_packagemeta.json
|
||||
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/"
|
||||
@@ -26,23 +29,8 @@ path="github.com/gohugoio/hugoTestModule2"
|
||||
"name": "mypackage",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
-- golden/package.json --
|
||||
-- golden/packages/hugoautogen/package.json --
|
||||
{
|
||||
"comments": {
|
||||
"dependencies": {
|
||||
"react-dom": "github.com/gohugoio/hugoTestModule2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "github.com/gohugoio/hugoTestModule2",
|
||||
"@babel/core": "github.com/gohugoio/hugoTestModule2",
|
||||
"@babel/preset-env": "github.com/gohugoio/hugoTestModule2",
|
||||
"postcss-cli": "github.com/gohugoio/hugoTestModule2",
|
||||
"tailwindcss": "project"
|
||||
},
|
||||
"foo": {
|
||||
"a": "b"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
@@ -50,11 +38,26 @@ path="github.com/gohugoio/hugoTestModule2"
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.5",
|
||||
"postcss-cli": "7.1.0",
|
||||
"postcss-cli": "7.1.0"
|
||||
},
|
||||
"name": "script-mod_npm_withexisting",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
-- golden/package.json --
|
||||
{
|
||||
"comments": {
|
||||
"foo": {
|
||||
"a": "b"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "2.2.0"
|
||||
},
|
||||
"name": "mypackage",
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.0",
|
||||
"workspaces": [
|
||||
"packages/hugoautogen"
|
||||
]
|
||||
}
|
||||
-- go.mod --
|
||||
module github.com/gohugoio/hugoTestModule
|
||||
|
||||
Reference in New Issue
Block a user