tailwind (empty) → 0.1.0.0
raw patch · 6 files changed
+340/−0 lines, 6 filesdep +aesondep +asyncdep +base
Dependencies added: aeson, async, base, bytestring, containers, data-default, deriving-aeson, directory, filepath, filepattern, lens, monad-logger, mtl, neat-interpolation, optparse-applicative, parser-combinators, profunctors, relude, safe-exceptions, tailwind, temporary, text, time, unliftio, which, with-utf8
Files
- CHANGELOG.md +5/−0
- LICENSE +21/−0
- README.md +11/−0
- exe/Main.hs +48/−0
- src/Web/Tailwind.hs +168/−0
- tailwind.cabal +87/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for tailwind (Haskell)++## 0.1.0.0 -- 2022-01-08++* First version.
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2021 Sridhar Ratnakumar++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,11 @@+# tailwind++Run TailwindCSS CLI [without needing](https://www.srid.ca/nojs) to touch anything JavaScript. No `input.css` or `tailwind.config.js` necessary.++```+cabal run tailwind-run -- 'src/**/.hs'+```++Compiles CSS classes in those file paths or patterns, and writes `./tailwind.css`. Pass `-w` to run in JIT watcher mode.++Used in Emanote, and Ema apps.
+ exe/Main.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import qualified Control.Applicative.Combinators.NonEmpty as NE+import Control.Lens.Operators+import Control.Monad.Logger.CallStack (runStdoutLoggingT)+import Data.Default (Default (def))+import Main.Utf8 (withUtf8)+import Options.Applicative+import System.FilePattern (FilePattern)+import Web.Tailwind++data Cli = Cli+ { content :: NonEmpty FilePattern,+ mode :: Mode+ }+ deriving (Eq, Show)++cliParser :: Parser Cli+cliParser = do+ content <- NE.some (argument str (metavar "SOURCES..."))+ mode <- modeParser+ pure Cli {..}++modeParser :: Parser Mode+modeParser = do+ bool Production JIT <$> switch (long "watch" <> short 'w' <> help "Run JIT")++main :: IO ()+main = do+ withUtf8 $ do+ cli <- execParser opts+ print cli+ runStdoutLoggingT $+ runTailwind $+ def+ & tailwindConfig . tailwindConfigContent .~ toList (content cli)+ & tailwindMode .~ mode cli+ where+ opts =+ info+ (cliParser <**> helper)+ ( fullDesc+ <> progDesc "Run Tailwind"+ )
+ src/Web/Tailwind.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Tailwind runner in Haskell+--+-- TODO: Add `main` with CLI parser.+-- > tailwind-run [-w] 'src/**/*.hs'+module Web.Tailwind+ ( -- * Runner+ runTailwind,++ -- * Types+ TailwindConfig (..),+ Tailwind (..),+ Mode (..),+ tailwindConfig,+ tailwindInput,+ tailwindOutput,+ tailwindConfigContent,+ tailwindMode,+ )+where++import Control.Lens.TH (makeLenses)+import Control.Monad.Logger (MonadLogger, logInfoN)+import Data.Aeson (encode)+import Data.ByteString (hPut)+import Data.Default (Default (def))+import Deriving.Aeson+import NeatInterpolation (text)+import System.CPUTime (getCPUTime)+import System.Directory (doesFileExist)+import System.IO (hClose)+import System.Which (staticWhich)+import Text.Printf (printf)+import qualified Text.Show+import UnliftIO (MonadUnliftIO, finally)+import UnliftIO.Directory (removeFile)+import UnliftIO.Process (callProcess)+import UnliftIO.Temporary (withSystemTempFile)++-- | Haskell version of tailwind.config.js+--+-- Only the subset we care to define, as some fields (eg: plugins) are defined+-- with arbitrary JS code.+data TailwindConfig = TailwindConfig+ { -- | List of source patterns that reference CSS classes+ _tailwindConfigContent :: [FilePath]+ }+ deriving (Generic)+ deriving+ (ToJSON)+ via CustomJSON+ '[ FieldLabelModifier+ '[StripPrefix "_tailwindConfig", CamelToSnake]+ ]+ TailwindConfig++newtype Css = Css {unCss :: Text}++data Mode = JIT | Production+ deriving (Eq, Show)++data Tailwind = Tailwind+ { _tailwindConfig :: TailwindConfig,+ _tailwindInput :: Css,+ _tailwindOutput :: FilePath,+ _tailwindMode :: Mode+ }++makeLenses ''TailwindConfig+makeLenses ''Tailwind++instance Default TailwindConfig where+ def =+ TailwindConfig+ { _tailwindConfigContent = []+ }++instance Default Tailwind where+ def =+ Tailwind+ { _tailwindConfig = def,+ _tailwindInput = def,+ _tailwindOutput = "tailwind.css",+ _tailwindMode = JIT+ }++instance Default Css where+ def =+ Css+ [text|+ @tailwind base;+ @tailwind components;+ @tailwind utilities;+ |]++instance Text.Show.Show TailwindConfig where+ show (decodeUtf8 . encode -> config) =+ -- Use `Object.assign` to merge JSON (produced in Haskell) with the rest of+ -- config (defined by raw JS; that cannot be JSON encoded)+ toString+ [text|+ module.exports =+ Object.assign(+ JSON.parse('${config}'),+ {+ theme: {+ extend: {},+ },+ plugins: [+ require('@tailwindcss/typography'),+ require('@tailwindcss/forms'),+ require('@tailwindcss/line-clamp'),+ require('@tailwindcss/aspect-ratio')+ ],+ })+ |]++tailwind :: FilePath+tailwind = $(staticWhich "tailwind")++modeArgs :: Mode -> [String]+modeArgs = \case+ JIT -> ["-w"]+ Production -> ["--minify"]++runTailwind :: (MonadUnliftIO m, MonadLogger m, HasCallStack) => Tailwind -> m ()+runTailwind Tailwind {..} = do+ withTmpFile (show _tailwindConfig) $ \configFile ->+ withTmpFile (unCss _tailwindInput) $ \inputFile ->+ callTailwind $ ["-c", configFile, "-i", inputFile, "-o", _tailwindOutput] <> modeArgs _tailwindMode+ when (_tailwindMode == JIT) $+ error "Tailwind exited unexpectedly!"++withTmpFile :: MonadUnliftIO m => Text -> (FilePath -> m a) -> m a+withTmpFile s f = do+ withSystemTempFile "ema-tailwind-tmpfile" $ \fp h -> do+ liftIO $ do+ putStrLn $ "$ cat " <> fp+ putTextLn s+ hPut h (encodeUtf8 s) >> hClose h+ f fp+ `finally` removeFile fp++callTailwind :: (MonadIO m, MonadLogger m) => [String] -> m ()+callTailwind args = do+ logInfoN $ "Running Tailwind compiler with args: " <> show args+ liftIO (doesFileExist tailwind) >>= \case+ True ->+ timeIt $ do+ callProcess tailwind args+ False ->+ error $ "Tailwind compiler not found at " <> toText tailwind++timeIt :: MonadIO m => m b -> m b+timeIt m = do+ t0 <- liftIO getCPUTime+ !x <- m+ t1 <- liftIO getCPUTime+ let diff :: Double = fromIntegral (t1 - t0) / (10 ^ (9 :: Integer))+ liftIO $ printf "Process duration: %0.3f ms\n" diff+ pure $! x
+ tailwind.cabal view
@@ -0,0 +1,87 @@+cabal-version: 2.4+name: tailwind+version: 0.1.0.0+license: MIT+copyright: 2022 Sridhar Ratnakumar+maintainer: srid@srid.ca+author: Sridhar Ratnakumar+category: Web++-- TODO: Before hackage release.+-- A short (one-line) description of the package.+synopsis: Tailwind wrapped in Haskell++-- A longer description of the package.+description: Run Tailwind from Haskell without touching JavaScript++-- A URL where users can report bugs.+-- bug-reports:++extra-source-files:+ CHANGELOG.md+ LICENSE+ README.md++common c+ mixins:+ base hiding (Prelude),+ relude (Relude as Prelude, Relude.Container.One),+ relude++ ghc-options:+ -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns++ build-depends:+ , base >=4.13.0.0 && <=4.17.0.0+ , data-default+ , filepath+ , filepattern+ , lens+ , monad-logger+ , relude++ default-extensions:+ FlexibleContexts+ FlexibleInstances+ KindSignatures+ LambdaCase+ MultiParamTypeClasses+ MultiWayIf+ OverloadedStrings+ ScopedTypeVariables+ TupleSections+ ViewPatterns++ default-language: Haskell2010++library+ import: c+ hs-source-dirs: src+ exposed-modules: Web.Tailwind+ build-depends:+ , aeson+ , async+ , bytestring+ , containers+ , deriving-aeson+ , directory+ , mtl+ , neat-interpolation+ , profunctors+ , safe-exceptions+ , temporary+ , text+ , time+ , unliftio+ , which+ , with-utf8++executable tailwind-run+ import: c+ hs-source-dirs: exe+ main-is: Main.hs+ build-depends:+ , optparse-applicative+ , parser-combinators+ , tailwind+ , with-utf8