hugo/modules/npm/package_builder.go
Bjørn Erik Pedersen f2946da9e8 Improve error messages, esp. when the server is running
* Add file context to minifier errors when publishing
* Misc fixes (see issues)
* Allow custom server error template in layouts/server/error.html

To get to this, this commit also cleans up and simplifies the code surrounding errors and files. This also removes the usage of `github.com/pkg/errors`, mostly because of https://github.com/pkg/errors/issues/223 -- but also because most of this is now built-in to Go.

Fixes #9852
Fixes #9857
Fixes #9863
2022-05-06 19:43:22 +02:00

237 lines
5.9 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/common/maps"
"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 {
// Preserve the original in package.hugo.json.
if err = hugio.CopyFile(fs, packageJSONName, files.FilenamePackageHugoJSON); err != nil {
return fmt.Errorf("npm pack: failed to copy package file: %w", err)
}
} else {
// Create one.
name := "project"
// Use the Hugo 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 fmt.Errorf("npm pack: failed to open package file: %w", err)
}
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 fmt.Errorf("npm pack: failed to open package file: %w", err)
}
b.Add(meta.Module, f)
f.Close()
}
if b.Err() != nil {
return fmt.Errorf("npm pack: failed to build: %w", b.Err())
}
// Replace the dependencies in the original template with the merged set.
b.originalPackageJSON[dependenciesKey] = b.dependencies
b.originalPackageJSON[devDependenciesKey] = b.devDependencies
var commentsm map[string]any
comments, found := b.originalPackageJSON["comments"]
if found {
commentsm = maps.ToStringMap(comments)
} else {
commentsm = make(map[string]any)
}
commentsm[dependenciesKey] = b.dependenciesComments
commentsm[devDependenciesKey] = b.devDependenciesComments
b.originalPackageJSON["comments"] = commentsm
// Write it out to the project package.json
packageJSONData := new(bytes.Buffer)
encoder := json.NewEncoder(packageJSONData)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", strings.Repeat(" ", 2))
if err := encoder.Encode(b.originalPackageJSON); err != nil {
return fmt.Errorf("npm pack: failed to marshal JSON: %w", err)
}
if err := afero.WriteFile(fs, packageJSONName, packageJSONData.Bytes(), 0666); err != nil {
return fmt.Errorf("npm pack: failed to write package.json: %w", err)
}
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),
}
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]any
devDependencies map[string]any
devDependenciesComments map[string]any
dependencies map[string]any
dependenciesComments map[string]any
}
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]any) {
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 := maps.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 := maps.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]any {
m := make(map[string]any)
err := json.Unmarshal(helpers.ReaderToBytes(r), &m)
if err != nil {
b.err = err
}
return m
}
func (b *packageBuilder) Err() error {
return b.err
}