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