github.com/pietrocarrara/hugo@v0.47.1/resource/postcss/postcss.go (about) 1 // Copyright 2018 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 postcss 15 16 import ( 17 "fmt" 18 "io" 19 "path/filepath" 20 21 "github.com/gohugoio/hugo/hugofs" 22 23 "github.com/mitchellh/mapstructure" 24 // "io/ioutil" 25 "os" 26 "os/exec" 27 28 "github.com/gohugoio/hugo/common/errors" 29 30 "github.com/gohugoio/hugo/resource" 31 ) 32 33 // Some of the options from https://github.com/postcss/postcss-cli 34 type Options struct { 35 36 // Set a custom path to look for a config file. 37 Config string 38 39 NoMap bool `mapstructure:"no-map"` // Disable the default inline sourcemaps 40 41 // Options for when not using a config file 42 Use string // List of postcss plugins to use 43 Parser string // Custom postcss parser 44 Stringifier string // Custom postcss stringifier 45 Syntax string // Custom postcss syntax 46 } 47 48 func DecodeOptions(m map[string]interface{}) (opts Options, err error) { 49 if m == nil { 50 return 51 } 52 err = mapstructure.WeakDecode(m, &opts) 53 return 54 } 55 56 func (opts Options) toArgs() []string { 57 var args []string 58 if opts.NoMap { 59 args = append(args, "--no-map") 60 } 61 if opts.Use != "" { 62 args = append(args, "--use", opts.Use) 63 } 64 if opts.Parser != "" { 65 args = append(args, "--parser", opts.Parser) 66 } 67 if opts.Stringifier != "" { 68 args = append(args, "--stringifier", opts.Stringifier) 69 } 70 if opts.Syntax != "" { 71 args = append(args, "--syntax", opts.Syntax) 72 } 73 return args 74 } 75 76 // Client is the client used to do PostCSS transformations. 77 type Client struct { 78 rs *resource.Spec 79 } 80 81 // New creates a new Client with the given specification. 82 func New(rs *resource.Spec) *Client { 83 return &Client{rs: rs} 84 } 85 86 type postcssTransformation struct { 87 options Options 88 rs *resource.Spec 89 } 90 91 func (t *postcssTransformation) Key() resource.ResourceTransformationKey { 92 return resource.NewResourceTransformationKey("postcss", t.options) 93 } 94 95 // Transform shells out to postcss-cli to do the heavy lifting. 96 // For this to work, you need some additional tools. To install them globally: 97 // npm install -g postcss-cli 98 // npm install -g autoprefixer 99 func (t *postcssTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { 100 101 const localPostCSSPath = "node_modules/postcss-cli/bin/" 102 const binaryName = "postcss" 103 104 // Try first in the project's node_modules. 105 csiBinPath := filepath.Join(t.rs.WorkingDir, localPostCSSPath, binaryName) 106 107 binary := csiBinPath 108 109 if _, err := exec.LookPath(binary); err != nil { 110 // Try PATH 111 binary = binaryName 112 if _, err := exec.LookPath(binary); err != nil { 113 // This may be on a CI server etc. Will fall back to pre-built assets. 114 return errors.FeatureNotAvailableErr 115 } 116 } 117 118 var configFile string 119 logger := t.rs.Logger 120 121 if t.options.Config != "" { 122 configFile = t.options.Config 123 } else { 124 configFile = "postcss.config.js" 125 } 126 127 configFile = filepath.Clean(configFile) 128 129 // We need an abolute filename to the config file. 130 if !filepath.IsAbs(configFile) { 131 // We resolve this against the virtual Work filesystem, to allow 132 // this config file to live in one of the themes if needed. 133 fi, err := t.rs.BaseFs.Work.Fs.Stat(configFile) 134 if err != nil { 135 if t.options.Config != "" { 136 // Only fail if the user specificed config file is not found. 137 return fmt.Errorf("postcss config %q not found: %s", configFile, err) 138 } 139 configFile = "" 140 } else { 141 configFile = fi.(hugofs.RealFilenameInfo).RealFilename() 142 } 143 } 144 145 var cmdArgs []string 146 147 if configFile != "" { 148 logger.INFO.Println("postcss: use config file", configFile) 149 cmdArgs = []string{"--config", configFile} 150 } 151 152 if optArgs := t.options.toArgs(); len(optArgs) > 0 { 153 cmdArgs = append(cmdArgs, optArgs...) 154 } 155 156 cmd := exec.Command(binary, cmdArgs...) 157 158 cmd.Stdout = ctx.To 159 cmd.Stderr = os.Stderr 160 161 stdin, err := cmd.StdinPipe() 162 if err != nil { 163 return err 164 } 165 166 go func() { 167 defer stdin.Close() 168 io.Copy(stdin, ctx.From) 169 }() 170 171 err = cmd.Run() 172 if err != nil { 173 return err 174 } 175 176 return nil 177 } 178 179 // Process transforms the given Resource with the PostCSS processor. 180 func (c *Client) Process(res resource.Resource, options Options) (resource.Resource, error) { 181 return c.rs.Transform( 182 res, 183 &postcssTransformation{rs: c.rs, options: options}, 184 ) 185 }