type configKey struct {
counter int32
ignoreModulesDoesNotExists bool
+ skipNpmCheck bool
}
// This is the root command.
Logger: r.logger,
Environment: r.environment,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
+ SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
Environment: r.environment,
Logger: r.logger,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
+ SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
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)
},
},
},
return err
}
client := conf.configs.ModulesClient
- return client.Get(args...)
+ if err := client.Get(args...); err != nil {
+ return err
+ }
+ return nil
}
},
},
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) {
"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"
)
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)
}
// 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 {
)
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"
}
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
import (
"bufio"
+ "encoding/json"
"errors"
"fmt"
"io/fs"
"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"
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 {
}
+ // 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/") {
package modules
import (
+ "path/filepath"
"testing"
qt "github.com/frankban/quicktest"
+ "github.com/spf13/afero"
)
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"},
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
// 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.
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"
dependenciesKey = "dependencies"
devDependenciesKey = "devDependencies"
- packageJSONName = "package.json"
+ packageJSONName = "package.json"
+ packageMetaJSONName = "hugo_packagemeta.json"
+)
- packageJSONTemplate = `{
- "name": "%s",
- "version": "%s"
-}`
+var (
+ workspacePackageJSON = filepath.Join(files.FolderPackagesHugoAutoGen, packageJSONName)
+ workspacePackageMetaJSON = filepath.Join(files.FolderPackagesHugoAutoGen, packageMetaJSONName)
)
-func Pack(sourceFs, assetsWithDuplicatesPreservedFs afero.Fs) error {
- var b *packageBuilder
+func Pack(sourceFs, assetsWithDuplicatesPreservedFs afero.Fs, mods modules.Modules) error {
+ b := &packageBuilder{
+ devDependencies: make(map[string]any),
+ devDependenciesComments: make(map[string]any),
+ dependencies: make(map[string]any),
+ dependenciesComments: make(map[string]any),
+ }
- // 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)
+ 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)
+ }
+ }
+
+ // 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
+ }
+
+ // 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
}
- } 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()
+
+ isPackageJSON := info.Name() == files.FilenamePackageJSON
+ isHugoJSON := info.Name() == files.FilenamePackageHugoJSON
+
+ if !isPackageJSON && !isHugoJSON {
+ return nil
}
- packageJSONContent := fmt.Sprintf(packageJSONTemplate, name, "0.1.0")
- if err = afero.WriteFile(sourceFs, files.FilenamePackageHugoJSON, []byte(packageJSONContent), 0o666); err != nil {
- return err
+
+ m := info.Meta()
+ if skipPackageJSON[m.ModulePath()] {
+ return nil
}
- fi, err = sourceFs.Stat(files.FilenamePackageHugoJSON)
- if err != nil {
- return err
+
+ // 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()
}
- 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)
+ if b.Err() != nil {
+ return fmt.Errorf("npm pack: failed to build: %w", b.Err())
}
- b = newPackageBuilder(meta.ModulePath(), f)
- f.Close()
+ // 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]
+ }
+ }
- d, err := assetsWithDuplicatesPreservedFs.Open(files.FolderJSConfig)
- if err != nil {
- return nil
+ 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)
+}
- fis, err := d.(fs.ReadDirFile).ReadDir(-1)
+// 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 {
- return fmt.Errorf("npm pack: failed to read assets: %w", err)
+ content := fmt.Sprintf("{\n \"workspaces\": [\n %q\n ]\n}\n", workspacePath)
+ return afero.WriteFile(fsys, packageJSONName, []byte(content), 0o666)
}
- for _, fi := range fis {
- if fi.IsDir() {
- continue
- }
+ if runtime.GOOS == "windows" {
+ data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
+ }
- if fi.Name() != files.FilenamePackageHugoJSON {
- continue
- }
+ // 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)
+ }
- meta := fi.(hugofs.FileMetaInfo).Meta()
+ indent := detectIndent(data)
+ quoted := fmt.Sprintf("%q", workspacePath)
- if meta.Filename == masterFilename {
- continue
+ 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
- f, err := meta.Open()
+ updated, err := json.MarshalIndent(pkg, "", indent)
if err != nil {
- return fmt.Errorf("npm pack: failed to open package file: %w", err)
+ return fmt.Errorf("npm pack: failed to marshal package.json with updated workspaces: %w", err)
}
- b.Add(meta.ModulePath(), f)
- f.Close()
+ 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)
}
- if b.Err() != nil {
- return fmt.Errorf("npm pack: failed to build: %w", b.Err())
+ // 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")
}
- // 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)
+ // 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 {
- commentsm = make(map[string]any)
+ insertion = indent + `"workspaces": [` + "\n" + indent + indent + quoted + "\n" + indent + "]\n"
}
- 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)
+ 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(b.originalPackageJSON); err != nil {
+ 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)
+}
- if err := afero.WriteFile(sourceFs, packageJSONName, packageJSONData.Bytes(), 0o666); err != nil {
- return fmt.Errorf("npm pack: failed to write package.json: %w", err)
+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 newPackageBuilder(source string, first io.Reader) *packageBuilder {
- b := &packageBuilder{
- devDependencies: make(map[string]any),
- devDependenciesComments: make(map[string]any),
- dependencies: make(map[string]any),
- dependenciesComments: make(map[string]any),
+func containsString(ss []string, s string) bool {
+ for _, v := range ss {
+ if v == s {
+ return true
+ }
}
+ return false
+}
- m := b.unmarshal(first)
- if b.err != nil {
- return b
+// 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
}
-
- b.addm(source, m)
- b.originalPackageJSON = m
-
- return b
+ 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
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 {
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)
+}
--- /dev/null
+// 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")
+ }
+}
-// 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.
// 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
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(`{
-# Test mod npm.
+# Test mod npm pack.
-dostounix golden/package.json
+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 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
-cmp package.json golden/package.json
+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": {
+ "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": {
+ "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": {
- "react-dom": "^16.13.1"
+ "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": {
- "@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"
--- /dev/null
+# 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"
+ }
+ }
+}
-# 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/"
"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"
},
"@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