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