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 }