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 }