build.go (5199B)
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 js
15
16 import (
17 "fmt"
18 "io/ioutil"
19 "os"
20 "path"
21 "path/filepath"
22 "regexp"
23 "strings"
24
25 "errors"
26
27 "github.com/spf13/afero"
28
29 "github.com/gohugoio/hugo/hugofs"
30
31 "github.com/gohugoio/hugo/common/herrors"
32 "github.com/gohugoio/hugo/common/text"
33
34 "github.com/gohugoio/hugo/hugolib/filesystems"
35 "github.com/gohugoio/hugo/media"
36 "github.com/gohugoio/hugo/resources/internal"
37
38 "github.com/evanw/esbuild/pkg/api"
39 "github.com/gohugoio/hugo/resources"
40 "github.com/gohugoio/hugo/resources/resource"
41 )
42
43 // Client context for ESBuild.
44 type Client struct {
45 rs *resources.Spec
46 sfs *filesystems.SourceFilesystem
47 }
48
49 // New creates a new client context.
50 func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
51 return &Client{
52 rs: rs,
53 sfs: fs,
54 }
55 }
56
57 type buildTransformation struct {
58 optsm map[string]any
59 c *Client
60 }
61
62 func (t *buildTransformation) Key() internal.ResourceTransformationKey {
63 return internal.NewResourceTransformationKey("jsbuild", t.optsm)
64 }
65
66 func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
67 ctx.OutMediaType = media.JavascriptType
68
69 opts, err := decodeOptions(t.optsm)
70 if err != nil {
71 return err
72 }
73
74 if opts.TargetPath != "" {
75 ctx.OutPath = opts.TargetPath
76 } else {
77 ctx.ReplaceOutPathExtension(".js")
78 }
79
80 src, err := ioutil.ReadAll(ctx.From)
81 if err != nil {
82 return err
83 }
84
85 opts.sourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
86 opts.resolveDir = t.c.rs.WorkingDir // where node_modules gets resolved
87 opts.contents = string(src)
88 opts.mediaType = ctx.InMediaType
89
90 buildOptions, err := toBuildOptions(opts)
91 if err != nil {
92 return err
93 }
94
95 buildOptions.Plugins, err = createBuildPlugins(t.c, opts)
96 if err != nil {
97 return err
98 }
99
100 if buildOptions.Sourcemap == api.SourceMapExternal && buildOptions.Outdir == "" {
101 buildOptions.Outdir, err = ioutil.TempDir(os.TempDir(), "compileOutput")
102 if err != nil {
103 return err
104 }
105 defer os.Remove(buildOptions.Outdir)
106 }
107
108 if opts.Inject != nil {
109 // Resolve the absolute filenames.
110 for i, ext := range opts.Inject {
111 impPath := filepath.FromSlash(ext)
112 if filepath.IsAbs(impPath) {
113 return fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
114 }
115
116 m := resolveComponentInAssets(t.c.rs.Assets.Fs, impPath)
117
118 if m == nil {
119 return fmt.Errorf("inject: file %q not found", ext)
120 }
121
122 opts.Inject[i] = m.Filename
123
124 }
125
126 buildOptions.Inject = opts.Inject
127
128 }
129
130 result := api.Build(buildOptions)
131
132 if len(result.Errors) > 0 {
133
134 createErr := func(msg api.Message) error {
135 loc := msg.Location
136 if loc == nil {
137 return errors.New(msg.Text)
138 }
139 path := loc.File
140 if path == stdinImporter {
141 path = ctx.SourcePath
142 }
143
144 errorMessage := msg.Text
145 errorMessage = strings.ReplaceAll(errorMessage, nsImportHugo+":", "")
146
147 var (
148 f afero.File
149 err error
150 )
151
152 if strings.HasPrefix(path, nsImportHugo) {
153 path = strings.TrimPrefix(path, nsImportHugo+":")
154 f, err = hugofs.Os.Open(path)
155 } else {
156 var fi os.FileInfo
157 fi, err = t.c.sfs.Fs.Stat(path)
158 if err == nil {
159 m := fi.(hugofs.FileMetaInfo).Meta()
160 path = m.Filename
161 f, err = m.Open()
162 }
163
164 }
165
166 if err == nil {
167 fe := herrors.
168 NewFileErrorFromName(errors.New(errorMessage), path).
169 UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}).
170 UpdateContent(f, nil)
171
172 f.Close()
173 return fe
174 }
175
176 return fmt.Errorf("%s", errorMessage)
177 }
178
179 var errors []error
180
181 for _, msg := range result.Errors {
182 errors = append(errors, createErr(msg))
183 }
184
185 // Return 1, log the rest.
186 for i, err := range errors {
187 if i > 0 {
188 t.c.rs.Logger.Errorf("js.Build failed: %s", err)
189 }
190 }
191
192 return errors[0]
193 }
194
195 if buildOptions.Sourcemap == api.SourceMapExternal {
196 content := string(result.OutputFiles[1].Contents)
197 symPath := path.Base(ctx.OutPath) + ".map"
198 re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
199 content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
200
201 if err = ctx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
202 return err
203 }
204 _, err := ctx.To.Write([]byte(content))
205 if err != nil {
206 return err
207 }
208 } else {
209 _, err := ctx.To.Write(result.OutputFiles[0].Contents)
210 if err != nil {
211 return err
212 }
213 }
214 return nil
215 }
216
217 // Process process esbuild transform
218 func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
219 return res.Transform(
220 &buildTransformation{c: c, optsm: opts},
221 )
222 }