URI: 
       postcss.go - hugo - [fork] hugo port for 9front
  HTML git clone https://git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
       postcss.go (6744B)
       ---
            1 // Copyright 2024 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 cssjs provides resource transformations backed by some popular JS based frameworks.
           15 package cssjs
           16 
           17 import (
           18         "bytes"
           19         "fmt"
           20         "io"
           21         "path/filepath"
           22         "strings"
           23 
           24         "github.com/gohugoio/hugo/common/collections"
           25         "github.com/gohugoio/hugo/common/hexec"
           26         "github.com/gohugoio/hugo/common/loggers"
           27 
           28         "github.com/gohugoio/hugo/common/hugo"
           29 
           30         "github.com/gohugoio/hugo/resources/internal"
           31         "github.com/spf13/cast"
           32 
           33         "github.com/mitchellh/mapstructure"
           34 
           35         "github.com/gohugoio/hugo/common/herrors"
           36         "github.com/gohugoio/hugo/resources"
           37         "github.com/gohugoio/hugo/resources/resource"
           38 )
           39 
           40 // NewPostCSSClient creates a new PostCSSClient with the given specification.
           41 func NewPostCSSClient(rs *resources.Spec) *PostCSSClient {
           42         return &PostCSSClient{rs: rs}
           43 }
           44 
           45 func decodePostCSSOptions(m map[string]any) (opts PostCSSOptions, err error) {
           46         if m == nil {
           47                 return
           48         }
           49         err = mapstructure.WeakDecode(m, &opts)
           50 
           51         if !opts.NoMap {
           52                 // There was for a long time a discrepancy between documentation and
           53                 // implementation for the noMap property, so we need to support both
           54                 // camel and snake case.
           55                 opts.NoMap = cast.ToBool(m["no-map"])
           56         }
           57 
           58         return
           59 }
           60 
           61 // PostCSSClient is the client used to do PostCSS transformations.
           62 type PostCSSClient struct {
           63         rs *resources.Spec
           64 }
           65 
           66 // Process transforms the given Resource with the PostCSS processor.
           67 func (c *PostCSSClient) Process(res resources.ResourceTransformer, options map[string]any) (resource.Resource, error) {
           68         return res.Transform(&postcssTransformation{rs: c.rs, optionsm: options})
           69 }
           70 
           71 type InlineImports struct {
           72         // Enable inlining of @import statements.
           73         // Does so recursively, but currently once only per file;
           74         // that is, it's not possible to import the same file in
           75         // different scopes (root, media query...)
           76         // Note that this import routine does not care about the CSS spec,
           77         // so you can have @import anywhere in the file.
           78         InlineImports bool
           79 
           80         // See issue https://github.com/gohugoio/hugo/issues/13719
           81         // Disable inlining of @import statements
           82         // This is currenty only used for css.TailwindCSS.
           83         DisableInlineImports bool
           84 
           85         // When InlineImports is enabled, we fail the build if an import cannot be resolved.
           86         // You can enable this to allow the build to continue and leave the import statement in place.
           87         // Note that the inline importer does not process url location or imports with media queries,
           88         // so those will be left as-is even without enabling this option.
           89         SkipInlineImportsNotFound bool
           90 }
           91 
           92 // Some of the options from https://github.com/postcss/postcss-cli
           93 type PostCSSOptions struct {
           94         // Set a custom path to look for a config file.
           95         Config string
           96 
           97         NoMap bool // Disable the default inline sourcemaps
           98 
           99         InlineImports `mapstructure:",squash"`
          100 
          101         // Options for when not using a config file
          102         Use         string // List of postcss plugins to use
          103         Parser      string //  Custom postcss parser
          104         Stringifier string // Custom postcss stringifier
          105         Syntax      string // Custom postcss syntax
          106 }
          107 
          108 func (opts PostCSSOptions) toArgs() []string {
          109         var args []string
          110         if opts.NoMap {
          111                 args = append(args, "--no-map")
          112         }
          113         if opts.Use != "" {
          114                 args = append(args, "--use")
          115                 args = append(args, strings.Fields(opts.Use)...)
          116         }
          117         if opts.Parser != "" {
          118                 args = append(args, "--parser", opts.Parser)
          119         }
          120         if opts.Stringifier != "" {
          121                 args = append(args, "--stringifier", opts.Stringifier)
          122         }
          123         if opts.Syntax != "" {
          124                 args = append(args, "--syntax", opts.Syntax)
          125         }
          126         return args
          127 }
          128 
          129 type postcssTransformation struct {
          130         optionsm map[string]any
          131         rs       *resources.Spec
          132 }
          133 
          134 func (t *postcssTransformation) Key() internal.ResourceTransformationKey {
          135         return internal.NewResourceTransformationKey("postcss", t.optionsm)
          136 }
          137 
          138 // Transform shells out to postcss-cli to do the heavy lifting.
          139 // For this to work, you need some additional tools. To install them globally:
          140 // npm install -g postcss-cli
          141 // npm install -g autoprefixer
          142 func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
          143         const binaryName = "postcss"
          144 
          145         infol := t.rs.Logger.InfoCommand(binaryName)
          146         infow := loggers.LevelLoggerToWriter(infol)
          147 
          148         ex := t.rs.ExecHelper
          149 
          150         var configFile string
          151 
          152         options, err := decodePostCSSOptions(t.optionsm)
          153         if err != nil {
          154                 return err
          155         }
          156 
          157         if options.Config != "" {
          158                 configFile = options.Config
          159         } else {
          160                 configFile = "postcss.config.js"
          161         }
          162 
          163         configFile = filepath.Clean(configFile)
          164 
          165         // We need an absolute filename to the config file.
          166         if !filepath.IsAbs(configFile) {
          167                 configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
          168                 if configFile == "" && options.Config != "" {
          169                         // Only fail if the user specified config file is not found.
          170                         return fmt.Errorf("postcss config %q not found", options.Config)
          171                 }
          172         }
          173 
          174         var cmdArgs []any
          175 
          176         if configFile != "" {
          177                 infol.Logf("use config file %q", configFile)
          178                 cmdArgs = []any{"--config", configFile}
          179         }
          180 
          181         if optArgs := options.toArgs(); len(optArgs) > 0 {
          182                 cmdArgs = append(cmdArgs, collections.StringSliceToInterfaceSlice(optArgs)...)
          183         }
          184 
          185         var errBuf bytes.Buffer
          186 
          187         stderr := io.MultiWriter(infow, &errBuf)
          188         cmdArgs = append(cmdArgs, hexec.WithStderr(stderr))
          189         cmdArgs = append(cmdArgs, hexec.WithStdout(ctx.To))
          190         cmdArgs = append(cmdArgs, hexec.WithEnviron(hugo.GetExecEnviron(t.rs.Cfg.BaseConfig().WorkingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)))
          191 
          192         cmd, err := ex.Npx(binaryName, cmdArgs...)
          193         if err != nil {
          194                 if hexec.IsNotFound(err) {
          195                         // This may be on a CI server etc. Will fall back to pre-built assets.
          196                         return &herrors.FeatureNotAvailableError{Cause: err}
          197                 }
          198                 return err
          199         }
          200 
          201         stdin, err := cmd.StdinPipe()
          202         if err != nil {
          203                 return err
          204         }
          205 
          206         src := ctx.From
          207 
          208         imp := newImportResolver(
          209                 ctx.From,
          210                 ctx.InPath,
          211                 options.InlineImports,
          212                 t.rs.Assets.Fs, t.rs.Logger, ctx.DependencyManager,
          213         )
          214 
          215         if options.InlineImports.InlineImports {
          216                 var err error
          217                 src, err = imp.resolve()
          218                 if err != nil {
          219                         return err
          220                 }
          221         }
          222 
          223         go func() {
          224                 defer stdin.Close()
          225                 io.Copy(stdin, src)
          226         }()
          227 
          228         err = cmd.Run()
          229         if err != nil {
          230                 if hexec.IsNotFound(err) {
          231                         return &herrors.FeatureNotAvailableError{
          232                                 Cause: err,
          233                         }
          234                 }
          235                 return imp.toFileError(errBuf.String())
          236         }
          237 
          238         return nil
          239 }