hugo/modules/npm/package_builder.go
Bjørn Erik Pedersen 85ba9bfffb 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
2020-09-13 20:55:29 +02:00

230 lines
5.6 KiB
Go

// 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
}