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
This commit is contained in:
Bjørn Erik Pedersen 2020-09-09 22:31:43 +02:00
parent 9df60b62f9
commit 85ba9bfffb
16 changed files with 721 additions and 46 deletions

View file

@ -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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -20,6 +20,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/modules"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -114,6 +116,8 @@ This is not needed if you only operate on modules inside /themes or if you have
RunE: nil, RunE: nil,
} }
cmd.AddCommand(newModNPMCmd(c))
cmd.AddCommand( cmd.AddCommand(
&cobra.Command{ &cobra.Command{
Use: "get", Use: "get",
@ -272,6 +276,15 @@ func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client
return f(com.hugo().ModulesClient) 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) { func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
com, err := initializeConfig(failOnNoConfig, false, &c.hugoBuilderCommon, c, nil) com, err := initializeConfig(failOnNoConfig, false, &c.hugoBuilderCommon, c, nil)
if err != nil { if err != nil {

58
commands/mod_npm.go Normal file
View file

@ -0,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
}

View file

@ -17,8 +17,15 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"os" "os"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugofs"
) )
const ( const (
@ -73,8 +80,23 @@ func NewInfo(environment string) Info {
} }
} }
func GetExecEnviron(cfg config.Provider) []string { func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
env := os.Environ() 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")) 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 return env
} }

View file

@ -24,6 +24,30 @@ Hugo Pipe's Babel requires the `@babel/cli` and `@babel/core` JavaScript package
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. 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 %}} {{% /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 ### Options
config [string] config [string]

View file

@ -26,6 +26,13 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
) )
const (
// The NPM package.json "template" file.
FilenamePackageHugoJSON = "package.hugo.json"
// The NPM package file.
FilenamePackageJSON = "package.json"
)
var ( var (
// This should be the only list of valid extensions for content files. // This should be the only list of valid extensions for content files.
contentFileExtensions = []string{ contentFileExtensions = []string{
@ -163,9 +170,12 @@ const (
ComponentFolderI18n = "i18n" ComponentFolderI18n = "i18n"
FolderResources = "resources" FolderResources = "resources"
FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc.
) )
var ( var (
JsConfigFolderMountPrefix = filepath.Join(ComponentFolderAssets, FolderJSConfig)
ComponentFolders = []string{ ComponentFolders = []string{
ComponentFolderArchetypes, ComponentFolderArchetypes,
ComponentFolderStatic, ComponentFolderStatic,

View file

@ -42,9 +42,6 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
(&rm).clean() (&rm).clean()
fromBase := files.ResolveComponentFolder(rm.From) fromBase := files.ResolveComponentFolder(rm.From)
if fromBase == "" {
panic("unrecognised component folder in" + rm.From)
}
if len(rm.To) < 2 { if len(rm.To) < 2 {
panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To)) panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To))

View file

@ -21,8 +21,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gohugoio/hugo/common/hugo"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/htesting"
@ -129,12 +127,6 @@ func TestWalkSymbolicLink(t *testing.T) {
}) })
t.Run("BasePath Fs", func(t *testing.T) { 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) c := qt.New(t)
docsFs := afero.NewBasePathFs(fs, docsDir) docsFs := afero.NewBasePathFs(fs, docsDir)

View file

@ -49,6 +49,9 @@ type BaseFs struct {
// SourceFilesystems contains the different source file systems. // SourceFilesystems contains the different source file systems.
*SourceFilesystems *SourceFilesystems
// The project source.
SourceFs afero.Fs
// The filesystem used to publish the rendered site. // The filesystem used to publish the rendered site.
// This usually maps to /my-project/public. // This usually maps to /my-project/public.
PublishFs afero.Fs PublishFs afero.Fs
@ -100,6 +103,23 @@ func (b *BaseFs) RelContentDir(filename string) string {
return filename 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 // SourceFilesystems contains the different source file systems. These can be
// composite file systems (theme and project etc.), and they have all root // composite file systems (theme and project etc.), and they have all root
// set to the source type the provides: data, i18n, static, layouts. // set to the source type the provides: data, i18n, static, layouts.
@ -346,8 +366,10 @@ func NewBase(p *paths.Paths, logger *loggers.Logger, options ...func(*BaseFs) er
} }
publishFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Destination, p.AbsPublishDir)) publishFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Destination, p.AbsPublishDir))
sourceFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Source, p.WorkingDir))
b := &BaseFs{ b := &BaseFs{
SourceFs: sourceFs,
PublishFs: publishFs, PublishFs: publishFs,
} }
@ -696,11 +718,16 @@ type filesystemsCollector struct {
func (c *filesystemsCollector) addDirs(rfs *hugofs.RootMappingFs) { func (c *filesystemsCollector) addDirs(rfs *hugofs.RootMappingFs) {
for _, componentFolder := range files.ComponentFolders { 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...)
} }
} }

View file

@ -22,6 +22,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/gohugoio/hugo/modules/npm"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -38,7 +40,6 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// https://github.com/gohugoio/hugo/issues/6730
func TestHugoModulesVariants(t *testing.T) { func TestHugoModulesVariants(t *testing.T) {
if !isCI() { if !isCI() {
t.Skip("skip (relative) long running modules test when running locally") t.Skip("skip (relative) long running modules test when running locally")
@ -60,8 +61,10 @@ path="github.com/gohugoio/hugoTestModule2"
newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) { newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) {
b := newTestSitesBuilder(t) 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) 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.Fs = hugofs.NewDefault(viper.New())
b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts)) b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts))
b.WithTemplates( b.WithTemplates(
@ -129,6 +132,158 @@ 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"
}`
})
})
} }
// TODO(bep) this fails when testmodBuilder is also building ... // TODO(bep) this fails when testmodBuilder is also building ...

View file

@ -873,6 +873,10 @@ func TestResourceChainPostCSS(t *testing.T) {
postcssConfig := ` postcssConfig := `
console.error("Hugo Environment:", process.env.HUGO_ENVIRONMENT ); 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 = { module.exports = {
plugins: [ plugins: [
@ -954,6 +958,8 @@ class-in-b {
// Make sure Node sees this. // Make sure Node sees this.
b.Assert(logBuf.String(), qt.Contains, "Hugo Environment: production") 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", ` b.AssertFileContent("public/index.html", `
Styles RelPermalink: /css/styles.css Styles RelPermalink: /css/styles.css

View file

@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@ -382,6 +383,11 @@ func (c *collector) applyMounts(moduleImport Import, mod *moduleAdapter) error {
return err return err
} }
mounts, err = c.mountCommonJSConfig(mod, mounts)
if err != nil {
return err
}
mod.mounts = mounts mod.mounts = mounts
return nil return nil
} }
@ -549,6 +555,43 @@ func (c *collector) loadModules() error {
return nil 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) { func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
var out []Mount var out []Mount
dir := owner.Dir() dir := owner.Dir()

View file

@ -56,7 +56,9 @@ func ApplyProjectConfigDefaults(cfg config.Provider, mod Module) error {
// the basic level. // the basic level.
componentsConfigured := make(map[string]bool) componentsConfigured := make(map[string]bool)
for _, mnt := range moda.mounts { for _, mnt := range moda.mounts {
componentsConfigured[mnt.Component()] = true if !strings.HasPrefix(mnt.Target, files.JsConfigFolderMountPrefix) {
componentsConfigured[mnt.Component()] = true
}
} }
type dirKeyComponent struct { type dirKeyComponent struct {
@ -318,12 +320,21 @@ type Mount struct {
Target string // relative target path, e.g. "assets/bootstrap/scss" Target string // relative target path, e.g. "assets/bootstrap/scss"
Lang string // any language code associated with this mount. Lang string // any language code associated with this mount.
} }
func (m Mount) Component() string { func (m Mount) Component() string {
return strings.Split(m.Target, fileSeparator)[0] 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 { func getStaticDirs(cfg config.Provider) []string {
var staticDirs []string var staticDirs []string
for i := -1; i <= 10; i++ { for i := -1; i <= 10; i++ {

View file

@ -0,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
}

View file

@ -0,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",
})
}

View file

@ -14,6 +14,7 @@
package babel package babel
import ( import (
"bytes"
"io" "io"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -27,7 +28,6 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -120,6 +120,9 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
var configFile string var configFile string
logger := t.rs.Logger logger := t.rs.Logger
var errBuf bytes.Buffer
infoW := loggers.LoggerToWriterWithPrefix(logger.INFO, "babel")
if t.options.Config != "" { if t.options.Config != "" {
configFile = t.options.Config configFile = t.options.Config
} else { } else {
@ -130,16 +133,10 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
// We need an abolute filename to the config file. // We need an abolute filename to the config file.
if !filepath.IsAbs(configFile) { if !filepath.IsAbs(configFile) {
// We resolve this against the virtual Work filesystem, to allow configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
// this config file to live in one of the themes if needed. if configFile == "" && t.options.Config != "" {
fi, err := t.rs.BaseFs.Work.Stat(configFile) // Only fail if the user specificed config file is not found.
if err != nil { return errors.Errorf("babel config %q not found:", configFile)
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()
} }
} }
@ -158,8 +155,8 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
cmd := exec.Command(binary, cmdArgs...) cmd := exec.Command(binary, cmdArgs...)
cmd.Stdout = ctx.To cmd.Stdout = ctx.To
cmd.Stderr = loggers.LoggerToWriterWithPrefix(logger.INFO, "babel") 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() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
@ -173,7 +170,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
err = cmd.Run() err = cmd.Run()
if err != nil { if err != nil {
return err return errors.Wrap(err, errBuf.String())
} }
return nil return nil

View file

@ -170,17 +170,11 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
// We need an abolute filename to the config file. // We need an abolute filename to the config file.
if !filepath.IsAbs(configFile) { if !filepath.IsAbs(configFile) {
// We resolve this against the virtual Work filesystem, to allow configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
// this config file to live in one of the themes if needed. if configFile == "" && t.options.Config != "" {
fi, err := t.rs.BaseFs.Work.Stat(configFile) // Only fail if the user specificed config file is not found.
if err != nil { return errors.Errorf("postcss config %q not found:", configFile)
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()
} }
} }
@ -202,7 +196,8 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
cmd.Stdout = ctx.To cmd.Stdout = ctx.To
cmd.Stderr = io.MultiWriter(infoW, &errBuf) 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() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {