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:
Bjørn Erik Pedersen
2026-03-23 16:00:50 +01:00
committed by GitHub
parent 3ff9b7f89f
commit d88a29e002
16 changed files with 1120 additions and 194 deletions
+3
View File
@@ -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
View File
@@ -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
}
},
},
+1 -1
View File
@@ -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) {
+8
View File
@@ -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 {
+6 -2
View File
@@ -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"
}
+1
View File
@@ -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
View File
@@ -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/") {
+59
View File
@@ -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"},
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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")
}
}
+9 -3
View File
@@ -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(`{
+199 -22
View File
@@ -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"
}
}
}
+22 -19
View File
@@ -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