ref: 85ba9bfffba9bfd0b095cb766f72700d4c211e31
parent: 9df60b62f9c4e36a269f0c6e9a69bee9dc691031
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Wed Sep 9 18:31:43 EDT 2020
Add "hugo mod npm pack" This commit also introduces a convention where these common JS config files, including `package.hugo.json`, gets mounted into: ``` assets/_jsconfig ´`` These files mapped to their real filename will be added to the environment when running PostCSS, Babel etc., so you can do `process.env.HUGO_FILE_TAILWIND_CONFIG_JS` to resolve the real filename. But do note that `assets` is a composite/union filesystem, so if your config file is not meant to be overridden, name them something specific. This commit also adds adds `workDir/node_modules` to `NODE_PATH` and `HUGO_WORKDIR` to the env when running the JS tools above. Fixes #7644 Fixes #7656 Fixes #7675
--- a/commands/mod.go
+++ b/commands/mod.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2020 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
"path/filepath"
"regexp"
+ "github.com/gohugoio/hugo/hugolib"
+
"github.com/gohugoio/hugo/modules"
"github.com/spf13/cobra"
)
@@ -114,6 +116,8 @@
RunE: nil,
}
+ cmd.AddCommand(newModNPMCmd(c))
+
cmd.AddCommand(
&cobra.Command{
Use: "get",
@@ -270,6 +274,15 @@
}
return f(com.hugo().ModulesClient)
+}
+
+func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
+ com, err := c.initConfig(true)
+ if err != nil {
+ return err
+ }
+
+ return f(com.hugo())
}
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
--- /dev/null
+++ b/commands/mod_npm.go
@@ -1,0 +1,58 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package commands
+
+import (
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/modules/npm"
+ "github.com/spf13/cobra"
+)
+
+func newModNPMCmd(c *modCmd) *cobra.Command {
+
+ cmd := &cobra.Command{
+ Use: "npm",
+ Short: "Various npm helpers.",
+ Long: `Various npm (Node package manager) helpers.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return c.withHugo(func(h *hugolib.HugoSites) error {
+ return nil
+ })
+ },
+ }
+
+ cmd.AddCommand(&cobra.Command{
+ Use: "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.
+
+On first run it creates a "package.hugo.json" in the project root if not alread there. This file will be used as a template file
+with the base dependency set.
+
+This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
+
+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.
+`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+
+ return c.withHugo(func(h *hugolib.HugoSites) error {
+ return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
+ })
+ },
+ })
+
+ return cmd
+}
--- a/common/hugo/hugo.go
+++ b/common/hugo/hugo.go
@@ -17,8 +17,15 @@
"fmt"
"html/template"
"os"
+ "path/filepath"
+ "strings"
+ "github.com/gohugoio/hugo/hugofs/files"
+
+ "github.com/spf13/afero"
+
"github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugofs"
)
const (
@@ -73,8 +80,23 @@
}
}
-func GetExecEnviron(cfg config.Provider) []string {
+func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
env := os.Environ()
+ nodepath := filepath.Join(workDir, "node_modules")
+ if np := os.Getenv("NODE_PATH"); np != "" {
+ nodepath = workDir + string(os.PathListSeparator) + np
+ }
+ config.SetEnvVars(&env, "NODE_PATH", nodepath)
+ config.SetEnvVars(&env, "HUGO_WORKDIR", workDir)
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
+ fis, err := afero.ReadDir(fs, files.FolderJSConfig)
+ if err == nil {
+ for _, fi := range fis {
+ key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
+ value := fi.(hugofs.FileMetaInfo).Meta().Filename()
+ config.SetEnvVars(&env, key, value)
+ }
+ }
+
return env
}
--- a/docs/content/en/hugo-pipes/babel.md
+++ b/docs/content/en/hugo-pipes/babel.md
@@ -24,6 +24,30 @@
If you are using the Hugo Snap package, Babel and plugin(s) need to be installed locally within your Hugo site directory, e.g., `npm install @babel/cli @babel/core --save-dev` without the `-g` flag.
{{% /note %}}
+
+### Config
+
+{{< new-in "v0.75.0" >}}
+
+In Hugo `v0.75` we improved the way we resolve JS configuration and dependencies. One of them is that we now adds the main project's `node_modules` to `NODE_PATH` when running Babel and similar tools. There are some known [issues](https://github.com/babel/babel/issues/5618) with Babel in this area, so if you have a `babel.config.js` living in a Hugo Module (and not in the project itself), we recommend using `require` to load the presets/plugins, e.g.:
+
+
+```js
+module.exports = {
+ presets: [
+ [
+ require('@babel/preset-env'),
+ {
+ useBuiltIns: 'entry',
+ corejs: 3
+ }
+ ]
+ ]
+};
+```
+
+
+
### Options
config [string]
--- a/hugofs/files/classifier.go
+++ b/hugofs/files/classifier.go
@@ -26,6 +26,13 @@
"github.com/spf13/afero"
)
+const (
+ // The NPM package.json "template" file.
+ FilenamePackageHugoJSON = "package.hugo.json"
+ // The NPM package file.
+ FilenamePackageJSON = "package.json"
+)
+
var (
// This should be the only list of valid extensions for content files.
contentFileExtensions = []string{
@@ -163,9 +170,12 @@
ComponentFolderI18n = "i18n"
FolderResources = "resources"
+ FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc.
)
var (
+ JsConfigFolderMountPrefix = filepath.Join(ComponentFolderAssets, FolderJSConfig)
+
ComponentFolders = []string{
ComponentFolderArchetypes,
ComponentFolderStatic,
--- a/hugofs/rootmapping_fs.go
+++ b/hugofs/rootmapping_fs.go
@@ -42,9 +42,6 @@
(&rm).clean()
fromBase := files.ResolveComponentFolder(rm.From)
- if fromBase == "" {
- panic("unrecognised component folder in" + rm.From)
- }
if len(rm.To) < 2 {
panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To))
--- a/hugofs/walk_test.go
+++ b/hugofs/walk_test.go
@@ -21,8 +21,6 @@
"strings"
"testing"
- "github.com/gohugoio/hugo/common/hugo"
-
"github.com/pkg/errors"
"github.com/gohugoio/hugo/htesting"
@@ -129,12 +127,6 @@
})
t.Run("BasePath Fs", func(t *testing.T) {
- if hugo.GoMinorVersion() < 12 {
- // https://github.com/golang/go/issues/30520
- // This is fixed in Go 1.13 and in the latest Go 1.12
- t.Skip("skip this for Go <= 1.11 due to a bug in Go's stdlib")
-
- }
c := qt.New(t)
docsFs := afero.NewBasePathFs(fs, docsDir)
--- a/hugolib/filesystems/basefs.go
+++ b/hugolib/filesystems/basefs.go
@@ -49,6 +49,9 @@
// SourceFilesystems contains the different source file systems.
*SourceFilesystems
+ // The project source.
+ SourceFs afero.Fs
+
// The filesystem used to publish the rendered site.
// This usually maps to /my-project/public.
PublishFs afero.Fs
@@ -100,6 +103,23 @@
return filename
}
+// ResolveJSConfigFile resolves the JS-related config file to a absolute
+// filename. One example of such would be postcss.config.js.
+func (fs *BaseFs) ResolveJSConfigFile(name string) string {
+ // First look in assets/_jsconfig
+ fi, err := fs.Assets.Fs.Stat(filepath.Join(files.FolderJSConfig, name))
+ if err == nil {
+ return fi.(hugofs.FileMetaInfo).Meta().Filename()
+ }
+ // Fall back to the work dir.
+ fi, err = fs.Work.Stat(name)
+ if err == nil {
+ return fi.(hugofs.FileMetaInfo).Meta().Filename()
+ }
+
+ return ""
+}
+
// SourceFilesystems contains the different source file systems. These can be
// composite file systems (theme and project etc.), and they have all root
// set to the source type the provides: data, i18n, static, layouts.
@@ -346,8 +366,10 @@
}
publishFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Destination, p.AbsPublishDir))
+ sourceFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Source, p.WorkingDir))
b := &BaseFs{
+ SourceFs: sourceFs,
PublishFs: publishFs,
}
@@ -696,11 +718,16 @@
func (c *filesystemsCollector) addDirs(rfs *hugofs.RootMappingFs) {
for _, componentFolder := range files.ComponentFolders {
- dirs, err := rfs.Dirs(componentFolder)
+ c.addDir(rfs, componentFolder)
+ }
- if err == nil {
- c.overlayDirs[componentFolder] = append(c.overlayDirs[componentFolder], dirs...)
- }
+}
+
+func (c *filesystemsCollector) addDir(rfs *hugofs.RootMappingFs, componentFolder string) {
+ dirs, err := rfs.Dirs(componentFolder)
+
+ if err == nil {
+ c.overlayDirs[componentFolder] = append(c.overlayDirs[componentFolder], dirs...)
}
}
--- a/hugolib/hugo_modules_test.go
+++ b/hugolib/hugo_modules_test.go
@@ -22,6 +22,8 @@
"testing"
"time"
+ "github.com/gohugoio/hugo/modules/npm"
+
"github.com/gohugoio/hugo/common/loggers"
"github.com/spf13/afero"
@@ -38,7 +40,6 @@
"github.com/spf13/viper"
)
-// https://github.com/gohugoio/hugo/issues/6730
func TestHugoModulesVariants(t *testing.T) {
if !isCI() {
t.Skip("skip (relative) long running modules test when running locally")
@@ -60,8 +61,10 @@
newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) {
b := newTestSitesBuilder(t)
- workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-variants")
+ tempDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-variants")
b.Assert(err, qt.IsNil)
+ workingDir := filepath.Join(tempDir, "myhugosite")
+ b.Assert(os.MkdirAll(workingDir, 0777), qt.IsNil)
b.Fs = hugofs.NewDefault(viper.New())
b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts))
b.WithTemplates(
@@ -127,6 +130,158 @@
Param from module: Rocks|
JS imported in module: |
`)
+ })
+
+ t.Run("Create package.json", func(t *testing.T) {
+
+ b, clean := newTestBuilder(t, "")
+ defer clean()
+
+ b.WithSourceFile("package.json", `{
+ "name": "mypack",
+ "version": "1.2.3",
+ "scripts": {},
+ "dependencies": {
+ "nonon": "error"
+ }
+}`)
+
+ b.WithSourceFile("package.hugo.json", `{
+ "name": "mypack",
+ "version": "1.2.3",
+ "scripts": {},
+ "dependencies": {
+ "foo": "1.2.3"
+ },
+ "devDependencies": {
+ "postcss-cli": "7.8.0",
+ "tailwindcss": "1.8.0"
+
+ }
+}`)
+
+ b.Build(BuildCfg{})
+ b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil)
+
+ b.AssertFileContentFn("package.json", func(s string) bool {
+ return s == `{
+ "comments": {
+ "dependencies": {
+ "foo": "project",
+ "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": "project",
+ "tailwindcss": "project"
+ }
+ },
+ "dependencies": {
+ "foo": "1.2.3",
+ "react-dom": "^16.13.1"
+ },
+ "devDependencies": {
+ "@babel/cli": "7.8.4",
+ "@babel/core": "7.9.0",
+ "@babel/preset-env": "7.9.5",
+ "postcss-cli": "7.8.0",
+ "tailwindcss": "1.8.0"
+ },
+ "name": "mypack",
+ "scripts": {},
+ "version": "1.2.3"
+}`
+ })
+ })
+
+ t.Run("Create package.json, no default", func(t *testing.T) {
+
+ b, clean := newTestBuilder(t, "")
+ defer clean()
+
+ b.WithSourceFile("package.json", `{
+ "name": "mypack",
+ "version": "1.2.3",
+ "scripts": {},
+ "dependencies": {
+ "moo": "1.2.3"
+ }
+}`)
+
+ b.Build(BuildCfg{})
+ b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil)
+
+ b.AssertFileContentFn("package.json", func(s string) bool {
+ return s == `{
+ "comments": {
+ "dependencies": {
+ "moo": "project",
+ "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": "github.com/gohugoio/hugoTestModule2"
+ }
+ },
+ "dependencies": {
+ "moo": "1.2.3",
+ "react-dom": "^16.13.1"
+ },
+ "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"
+ },
+ "name": "mypack",
+ "scripts": {},
+ "version": "1.2.3"
+}`
+ })
+ })
+
+ t.Run("Create package.json, no default, no package.json", func(t *testing.T) {
+
+ b, clean := newTestBuilder(t, "")
+ defer clean()
+
+ b.Build(BuildCfg{})
+ b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil)
+
+ b.AssertFileContentFn("package.json", func(s string) bool {
+ return s == `{
+ "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": "github.com/gohugoio/hugoTestModule2"
+ }
+ },
+ "dependencies": {
+ "react-dom": "^16.13.1"
+ },
+ "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"
+ },
+ "name": "myhugosite",
+ "version": "0.1.0"
+}`
+ })
})
}
--- a/hugolib/resource_chain_test.go
+++ b/hugolib/resource_chain_test.go
@@ -873,7 +873,11 @@
postcssConfig := `
console.error("Hugo Environment:", process.env.HUGO_ENVIRONMENT );
+// https://github.com/gohugoio/hugo/issues/7656
+console.error("package.json:", process.env.HUGO_FILE_PACKAGE_JSON );
+console.error("PostCSS Config File:", process.env.HUGO_FILE_POSTCSS_CONFIG_JS );
+
module.exports = {
plugins: [
require('tailwindcss')
@@ -954,6 +958,8 @@
// Make sure Node sees this.
b.Assert(logBuf.String(), qt.Contains, "Hugo Environment: production")
+ b.Assert(logBuf.String(), qt.Contains, fmt.Sprintf("PostCSS Config File: %s/postcss.config.js", workDir))
+ b.Assert(logBuf.String(), qt.Contains, fmt.Sprintf("package.json: %s/package.json", workDir))
b.AssertFileContent("public/index.html", `
Styles RelPermalink: /css/styles.css
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -18,6 +18,7 @@
"fmt"
"os"
"path/filepath"
+ "regexp"
"strings"
"time"
@@ -382,6 +383,11 @@
return err
}
+ mounts, err = c.mountCommonJSConfig(mod, mounts)
+ if err != nil {
+ return err
+ }
+
mod.mounts = mounts
return nil
}
@@ -547,6 +553,43 @@
}
c.gomods = modules
return nil
+}
+
+// Matches postcss.config.js etc.
+var commonJSConfigs = regexp.MustCompile(`(babel|postcss|tailwind)\.config\.js`)
+
+func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
+ for _, m := range mounts {
+ if strings.HasPrefix(m.Target, files.JsConfigFolderMountPrefix) {
+ // This follows the convention of the other component types (assets, content, etc.),
+ // if one or more is specificed by the user, we skip the defaults.
+ // These mounts were added to Hugo in 0.75.
+ return mounts, nil
+ }
+ }
+
+ // Mount the common JS config files.
+ fis, err := afero.ReadDir(c.fs, owner.Dir())
+ if err != nil {
+ return mounts, err
+ }
+
+ for _, fi := range fis {
+ n := fi.Name()
+
+ should := n == files.FilenamePackageHugoJSON || n == files.FilenamePackageJSON
+ should = should || commonJSConfigs.MatchString(n)
+
+ if should {
+ mounts = append(mounts, Mount{
+ Source: n,
+ Target: filepath.Join(files.ComponentFolderAssets, files.FolderJSConfig, n),
+ })
+ }
+
+ }
+
+ return mounts, nil
}
func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
--- a/modules/config.go
+++ b/modules/config.go
@@ -56,7 +56,9 @@
// the basic level.
componentsConfigured := make(map[string]bool)
for _, mnt := range moda.mounts {
- componentsConfigured[mnt.Component()] = true
+ if !strings.HasPrefix(mnt.Target, files.JsConfigFolderMountPrefix) {
+ componentsConfigured[mnt.Component()] = true
+ }
}
type dirKeyComponent struct {
@@ -318,10 +320,19 @@
Target string // relative target path, e.g. "assets/bootstrap/scss"
Lang string // any language code associated with this mount.
+
}
func (m Mount) Component() string {
return strings.Split(m.Target, fileSeparator)[0]
+}
+
+func (m Mount) ComponentAndName() (string, string) {
+ k := strings.Index(m.Target, fileSeparator)
+ if k == -1 {
+ return m.Target, ""
+ }
+ return m.Target[:k], m.Target[k+1:]
}
func getStaticDirs(cfg config.Provider) []string {
--- /dev/null
+++ b/modules/npm/package_builder.go
@@ -1,0 +1,230 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package npm
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/gohugoio/hugo/hugofs/files"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/afero"
+
+ "github.com/spf13/cast"
+
+ "github.com/gohugoio/hugo/helpers"
+)
+
+const (
+ dependenciesKey = "dependencies"
+ devDependenciesKey = "devDependencies"
+
+ packageJSONName = "package.json"
+
+ packageJSONTemplate = `{
+ "name": "%s",
+ "version": "%s"
+}`
+)
+
+func Pack(fs afero.Fs, fis []hugofs.FileMetaInfo) error {
+
+ var b *packageBuilder
+
+ // Have a package.hugo.json?
+ fi, err := fs.Stat(files.FilenamePackageHugoJSON)
+ if err != nil {
+ // Have a package.json?
+ fi, err = fs.Stat(packageJSONName)
+ if err != nil {
+ // Create one.
+ name := "project"
+ // Use the Hugo site's folder name as the default name.
+ // The owner can change it later.
+ rfi, err := fs.Stat("")
+ if err == nil {
+ name = rfi.Name()
+ }
+ packageJSONContent := fmt.Sprintf(packageJSONTemplate, name, "0.1.0")
+ if err = afero.WriteFile(fs, files.FilenamePackageHugoJSON, []byte(packageJSONContent), 0666); err != nil {
+ return err
+ }
+ fi, err = fs.Stat(files.FilenamePackageHugoJSON)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ meta := fi.(hugofs.FileMetaInfo).Meta()
+ masterFilename := meta.Filename()
+ f, err := meta.Open()
+ if err != nil {
+ return errors.Wrap(err, "npm pack: failed to open package file")
+ }
+ b = newPackageBuilder(meta.Module(), f)
+ f.Close()
+
+ for _, fi := range fis {
+ if fi.IsDir() {
+ // We only care about the files in the root.
+ 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 errors.Wrap(err, "npm pack: failed to open package file")
+ }
+ b.Add(meta.Module(), f)
+ f.Close()
+ }
+
+ if b.Err() != nil {
+ return errors.Wrap(b.Err(), "npm pack: failed to build")
+ }
+
+ // 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]interface{}
+ comments, found := b.originalPackageJSON["comments"]
+ if found {
+ commentsm = cast.ToStringMap(comments)
+ } else {
+ commentsm = make(map[string]interface{})
+ }
+ commentsm[dependenciesKey] = b.dependenciesComments
+ commentsm[devDependenciesKey] = b.devDependenciesComments
+ b.originalPackageJSON["comments"] = commentsm
+
+ // Write it out to the project package.json
+ packageJSONData, err := json.MarshalIndent(b.originalPackageJSON, "", " ")
+ if err != nil {
+ return errors.Wrap(err, "npm pack: failed to marshal JSON")
+ }
+
+ if err := afero.WriteFile(fs, packageJSONName, packageJSONData, 0666); err != nil {
+ return errors.Wrap(err, "npm pack: failed to write package.json")
+ }
+
+ return nil
+
+}
+
+func newPackageBuilder(source string, first io.Reader) *packageBuilder {
+ b := &packageBuilder{
+ devDependencies: make(map[string]interface{}),
+ devDependenciesComments: make(map[string]interface{}),
+ dependencies: make(map[string]interface{}),
+ dependenciesComments: make(map[string]interface{}),
+ }
+
+ m := b.unmarshal(first)
+ if b.err != nil {
+ return b
+ }
+
+ b.addm(source, m)
+ b.originalPackageJSON = m
+
+ return b
+}
+
+type packageBuilder struct {
+ err error
+
+ // The original package.hugo.json.
+ originalPackageJSON map[string]interface{}
+
+ devDependencies map[string]interface{}
+ devDependenciesComments map[string]interface{}
+ dependencies map[string]interface{}
+ dependenciesComments map[string]interface{}
+}
+
+func (b *packageBuilder) Add(source string, r io.Reader) *packageBuilder {
+ if b.err != nil {
+ return b
+ }
+
+ m := b.unmarshal(r)
+ if b.err != nil {
+ return b
+ }
+
+ b.addm(source, m)
+
+ return b
+}
+
+func (b *packageBuilder) addm(source string, m map[string]interface{}) {
+ if source == "" {
+ 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.
+ if devDeps, found := m[devDependenciesKey]; found {
+ mm := cast.ToStringMapString(devDeps)
+ for k, v := range mm {
+ if _, added := b.devDependencies[k]; !added {
+ b.devDependencies[k] = v
+ b.devDependenciesComments[k] = source
+ }
+ }
+ }
+
+ if deps, found := m[dependenciesKey]; found {
+ mm := cast.ToStringMapString(deps)
+ for k, v := range mm {
+ if _, added := b.dependencies[k]; !added {
+ b.dependencies[k] = v
+ b.dependenciesComments[k] = source
+ }
+ }
+ }
+
+}
+
+func (b *packageBuilder) unmarshal(r io.Reader) map[string]interface{} {
+ m := make(map[string]interface{})
+ err := json.Unmarshal(helpers.ReaderToBytes(r), &m)
+ if err != nil {
+ b.err = err
+ }
+ return m
+}
+
+func (b *packageBuilder) Err() error {
+ return b.err
+}
--- /dev/null
+++ b/modules/npm/package_builder_test.go
@@ -1,0 +1,95 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package npm
+
+import (
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+const templ = `{
+ "name": "foo",
+ "version": "0.1.1",
+ "scripts": {},
+ "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"
+ }
+}`
+
+func TestPackageBuilder(t *testing.T) {
+ c := qt.New(t)
+
+ b := newPackageBuilder("", strings.NewReader(templ))
+ c.Assert(b.Err(), qt.IsNil)
+
+ b.Add("mymod", strings.NewReader(`{
+"dependencies": {
+ "react-dom": "9.1.1",
+ "add1": "1.1.1"
+},
+"devDependencies": {
+ "tailwindcss": "error",
+ "add2": "2.1.1"
+}
+}`))
+
+ b.Add("mymod", strings.NewReader(`{
+"dependencies": {
+ "react-dom": "error",
+ "add1": "error",
+ "add3": "3.1.1"
+},
+"devDependencies": {
+ "tailwindcss": "error",
+ "add2": "error",
+ "add4": "4.1.1"
+
+}
+}`))
+
+ c.Assert(b.Err(), qt.IsNil)
+
+ c.Assert(b.dependencies, qt.DeepEquals, map[string]interface{}{
+ "@babel/cli": "7.8.4",
+ "add1": "1.1.1",
+ "add3": "3.1.1",
+ "@babel/core": "7.9.0",
+ "@babel/preset-env": "7.9.5",
+ "react-dom": "1.1.1",
+ "tailwindcss": "1.2.0",
+ })
+
+ c.Assert(b.devDependencies, qt.DeepEquals, map[string]interface{}{
+ "tailwindcss": "1.2.0",
+ "@babel/cli": "7.8.4",
+ "@babel/core": "7.9.0",
+ "add2": "2.1.1",
+ "add4": "4.1.1",
+ "@babel/preset-env": "7.9.5",
+ "postcss-cli": "7.1.0",
+ })
+}
--- a/resources/resource_transformers/babel/babel.go
+++ b/resources/resource_transformers/babel/babel.go
@@ -14,6 +14,7 @@
package babel
import (
+ "bytes"
"io"
"os/exec"
"path/filepath"
@@ -27,7 +28,6 @@
"github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/pkg/errors"
@@ -120,6 +120,9 @@
var configFile string
logger := t.rs.Logger
+ var errBuf bytes.Buffer
+ infoW := loggers.LoggerToWriterWithPrefix(logger.INFO, "babel")
+
if t.options.Config != "" {
configFile = t.options.Config
} else {
@@ -130,16 +133,10 @@
// We need an abolute filename to the config file.
if !filepath.IsAbs(configFile) {
- // We resolve this against the virtual Work filesystem, to allow
- // this config file to live in one of the themes if needed.
- fi, err := t.rs.BaseFs.Work.Stat(configFile)
- if err != nil {
- if t.options.Config != "" {
- // Only fail if the user specificed config file is not found.
- return errors.Wrapf(err, "babel config %q not found:", configFile)
- }
- } else {
- configFile = fi.(hugofs.FileMetaInfo).Meta().Filename()
+ configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
+ if configFile == "" && t.options.Config != "" {
+ // Only fail if the user specificed config file is not found.
+ return errors.Errorf("babel config %q not found:", configFile)
}
}
@@ -158,8 +155,8 @@
cmd := exec.Command(binary, cmdArgs...)
cmd.Stdout = ctx.To
- cmd.Stderr = loggers.LoggerToWriterWithPrefix(logger.INFO, "babel")
- cmd.Env = hugo.GetExecEnviron(t.rs.Cfg)
+ cmd.Stderr = io.MultiWriter(infoW, &errBuf)
+ cmd.Env = hugo.GetExecEnviron(t.rs.WorkingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)
stdin, err := cmd.StdinPipe()
if err != nil {
@@ -173,7 +170,7 @@
err = cmd.Run()
if err != nil {
- return err
+ return errors.Wrap(err, errBuf.String())
}
return nil
--- a/resources/resource_transformers/postcss/postcss.go
+++ b/resources/resource_transformers/postcss/postcss.go
@@ -170,17 +170,11 @@
// We need an abolute filename to the config file.
if !filepath.IsAbs(configFile) {
- // We resolve this against the virtual Work filesystem, to allow
- // this config file to live in one of the themes if needed.
- fi, err := t.rs.BaseFs.Work.Stat(configFile)
- if err != nil {
- if t.options.Config != "" {
- // Only fail if the user specificed config file is not found.
- return errors.Wrapf(err, "postcss config %q not found:", configFile)
- }
- configFile = ""
- } else {
- configFile = fi.(hugofs.FileMetaInfo).Meta().Filename()
+ configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
+ if configFile == "" && t.options.Config != "" {
+ // Only fail if the user specificed config file is not found.
+ return errors.Errorf("postcss config %q not found:", configFile)
+
}
}
@@ -202,7 +196,8 @@
cmd.Stdout = ctx.To
cmd.Stderr = io.MultiWriter(infoW, &errBuf)
- cmd.Env = hugo.GetExecEnviron(t.rs.Cfg)
+
+ cmd.Env = hugo.GetExecEnviron(t.rs.WorkingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)
stdin, err := cmd.StdinPipe()
if err != nil {