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  }