github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/modules/npm/package_builder.go (about)

     1  // Copyright 2020 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package npm
    15  
    16  import (
    17  	"bytes"
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"strings"
    22  
    23  	"github.com/gohugoio/hugo/common/hugio"
    24  
    25  	"github.com/gohugoio/hugo/hugofs/files"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	"github.com/gohugoio/hugo/hugofs"
    30  	"github.com/spf13/afero"
    31  
    32  	"github.com/gohugoio/hugo/common/maps"
    33  
    34  	"github.com/gohugoio/hugo/helpers"
    35  )
    36  
    37  const (
    38  	dependenciesKey    = "dependencies"
    39  	devDependenciesKey = "devDependencies"
    40  
    41  	packageJSONName = "package.json"
    42  
    43  	packageJSONTemplate = `{
    44    "name": "%s",
    45    "version": "%s"
    46  }`
    47  )
    48  
    49  func Pack(fs afero.Fs, fis []hugofs.FileMetaInfo) error {
    50  	var b *packageBuilder
    51  
    52  	// Have a package.hugo.json?
    53  	fi, err := fs.Stat(files.FilenamePackageHugoJSON)
    54  	if err != nil {
    55  		// Have a package.json?
    56  		fi, err = fs.Stat(packageJSONName)
    57  		if err == nil {
    58  			// Preserve the original in package.hugo.json.
    59  			if err = hugio.CopyFile(fs, packageJSONName, files.FilenamePackageHugoJSON); err != nil {
    60  				return errors.Wrap(err, "npm pack: failed to copy package file")
    61  			}
    62  		} else {
    63  			// Create one.
    64  			name := "project"
    65  			// Use the Hugo site's folder name as the default name.
    66  			// The owner can change it later.
    67  			rfi, err := fs.Stat("")
    68  			if err == nil {
    69  				name = rfi.Name()
    70  			}
    71  			packageJSONContent := fmt.Sprintf(packageJSONTemplate, name, "0.1.0")
    72  			if err = afero.WriteFile(fs, files.FilenamePackageHugoJSON, []byte(packageJSONContent), 0666); err != nil {
    73  				return err
    74  			}
    75  			fi, err = fs.Stat(files.FilenamePackageHugoJSON)
    76  			if err != nil {
    77  				return err
    78  			}
    79  		}
    80  	}
    81  
    82  	meta := fi.(hugofs.FileMetaInfo).Meta()
    83  	masterFilename := meta.Filename
    84  	f, err := meta.Open()
    85  	if err != nil {
    86  		return errors.Wrap(err, "npm pack: failed to open package file")
    87  	}
    88  	b = newPackageBuilder(meta.Module, f)
    89  	f.Close()
    90  
    91  	for _, fi := range fis {
    92  		if fi.IsDir() {
    93  			// We only care about the files in the root.
    94  			continue
    95  		}
    96  
    97  		if fi.Name() != files.FilenamePackageHugoJSON {
    98  			continue
    99  		}
   100  
   101  		meta := fi.(hugofs.FileMetaInfo).Meta()
   102  
   103  		if meta.Filename == masterFilename {
   104  			continue
   105  		}
   106  
   107  		f, err := meta.Open()
   108  		if err != nil {
   109  			return errors.Wrap(err, "npm pack: failed to open package file")
   110  		}
   111  		b.Add(meta.Module, f)
   112  		f.Close()
   113  	}
   114  
   115  	if b.Err() != nil {
   116  		return errors.Wrap(b.Err(), "npm pack: failed to build")
   117  	}
   118  
   119  	// Replace the dependencies in the original template with the merged set.
   120  	b.originalPackageJSON[dependenciesKey] = b.dependencies
   121  	b.originalPackageJSON[devDependenciesKey] = b.devDependencies
   122  	var commentsm map[string]interface{}
   123  	comments, found := b.originalPackageJSON["comments"]
   124  	if found {
   125  		commentsm = maps.ToStringMap(comments)
   126  	} else {
   127  		commentsm = make(map[string]interface{})
   128  	}
   129  	commentsm[dependenciesKey] = b.dependenciesComments
   130  	commentsm[devDependenciesKey] = b.devDependenciesComments
   131  	b.originalPackageJSON["comments"] = commentsm
   132  
   133  	// Write it out to the project package.json
   134  	packageJSONData := new(bytes.Buffer)
   135  	encoder := json.NewEncoder(packageJSONData)
   136  	encoder.SetEscapeHTML(false)
   137  	encoder.SetIndent("", strings.Repeat(" ", 2))
   138  	if err := encoder.Encode(b.originalPackageJSON); err != nil {
   139  		return errors.Wrap(err, "npm pack: failed to marshal JSON")
   140  	}
   141  
   142  	if err := afero.WriteFile(fs, packageJSONName, packageJSONData.Bytes(), 0666); err != nil {
   143  		return errors.Wrap(err, "npm pack: failed to write package.json")
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  func newPackageBuilder(source string, first io.Reader) *packageBuilder {
   150  	b := &packageBuilder{
   151  		devDependencies:         make(map[string]interface{}),
   152  		devDependenciesComments: make(map[string]interface{}),
   153  		dependencies:            make(map[string]interface{}),
   154  		dependenciesComments:    make(map[string]interface{}),
   155  	}
   156  
   157  	m := b.unmarshal(first)
   158  	if b.err != nil {
   159  		return b
   160  	}
   161  
   162  	b.addm(source, m)
   163  	b.originalPackageJSON = m
   164  
   165  	return b
   166  }
   167  
   168  type packageBuilder struct {
   169  	err error
   170  
   171  	// The original package.hugo.json.
   172  	originalPackageJSON map[string]interface{}
   173  
   174  	devDependencies         map[string]interface{}
   175  	devDependenciesComments map[string]interface{}
   176  	dependencies            map[string]interface{}
   177  	dependenciesComments    map[string]interface{}
   178  }
   179  
   180  func (b *packageBuilder) Add(source string, r io.Reader) *packageBuilder {
   181  	if b.err != nil {
   182  		return b
   183  	}
   184  
   185  	m := b.unmarshal(r)
   186  	if b.err != nil {
   187  		return b
   188  	}
   189  
   190  	b.addm(source, m)
   191  
   192  	return b
   193  }
   194  
   195  func (b *packageBuilder) addm(source string, m map[string]interface{}) {
   196  	if source == "" {
   197  		source = "project"
   198  	}
   199  
   200  	// The version selection is currently very simple.
   201  	// We may consider minimal version selection or something
   202  	// after testing this out.
   203  	//
   204  	// But for now, the first version string for a given dependency wins.
   205  	// These packages will be added by order of import (project, module1, module2...),
   206  	// so that should at least give the project control over the situation.
   207  	if devDeps, found := m[devDependenciesKey]; found {
   208  		mm := maps.ToStringMapString(devDeps)
   209  		for k, v := range mm {
   210  			if _, added := b.devDependencies[k]; !added {
   211  				b.devDependencies[k] = v
   212  				b.devDependenciesComments[k] = source
   213  			}
   214  		}
   215  	}
   216  
   217  	if deps, found := m[dependenciesKey]; found {
   218  		mm := maps.ToStringMapString(deps)
   219  		for k, v := range mm {
   220  			if _, added := b.dependencies[k]; !added {
   221  				b.dependencies[k] = v
   222  				b.dependenciesComments[k] = source
   223  			}
   224  		}
   225  	}
   226  }
   227  
   228  func (b *packageBuilder) unmarshal(r io.Reader) map[string]interface{} {
   229  	m := make(map[string]interface{})
   230  	err := json.Unmarshal(helpers.ReaderToBytes(r), &m)
   231  	if err != nil {
   232  		b.err = err
   233  	}
   234  	return m
   235  }
   236  
   237  func (b *packageBuilder) Err() error {
   238  	return b.err
   239  }