package_builder.go (6017B)
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 }