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 }