├── .envrc ├── .ghcid ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── format ├── repl ├── run └── run-via-tmux ├── default.nix ├── exe └── Main.hs ├── flake.lock ├── flake.nix ├── src └── Web │ └── Tailwind.hs ├── tailwind.cabal └── tests └── Main.hs /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.ghcid: -------------------------------------------------------------------------------- 1 | --warnings 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | nix: 11 | runs-on: ${{ matrix.system }} 12 | strategy: 13 | matrix: 14 | system: [x86_64-linux, aarch64-darwin] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: om ci run --systems "${{ matrix.system }}" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-* 3 | cabal-dev 4 | *.o 5 | *.hi 6 | *.hie 7 | *.chi 8 | *.chs.h 9 | *.dyn_o 10 | *.dyn_hi 11 | .hpc 12 | .hsenv 13 | .cabal-sandbox/ 14 | cabal.sandbox.config 15 | *.prof 16 | *.aux 17 | *.hp 18 | *.eventlog 19 | .stack-work/ 20 | cabal.project.local 21 | cabal.project.local~ 22 | .HTF/ 23 | .ghc.environment.* 24 | result 25 | result-* 26 | 27 | .direnv 28 | 29 | tailwind.css -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "haskell.haskell", 6 | "bbenoist.nix", 7 | "jnoortheen.nix-ide" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": true, 3 | "editor.formatOnSave": true, 4 | "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix" 5 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Ghcid", 8 | "type": "shell", 9 | // You may also use bin/run-via-tmux if you have tmux 10 | // This is useful if you often see ghost ghcid left behind by VSCode reloads. 11 | "command": "bin/run", 12 | "args": [], 13 | "problemMatcher": [], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "runOptions": { 19 | // "runOn": "folderOpen" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for tailwind (Haskell) 2 | 3 | ## Unreleased (0.4.0.0) 4 | 5 | - Nix 6 | - GHC 9 support 7 | - Explicitly specify nixpkgs input in flake 8 | - Log path to the tailwind binary used. 9 | - #13: Allowing passing plugins in CLI 10 | - #23: Due to upstream change, we now look for `tailwindcss` binary 11 | 12 | ## 0.3.0.0 13 | 14 | - Nix: 15 | - Switch to official nixpkgs package for TailwindCSS and its plugins 16 | - Use relude 1.0 17 | - Switch from lens to `optics-core` 18 | 19 | ## 0.2.0.0 20 | 21 | - Add `--output` 22 | - Fix quoted source paths breaking tailwind.config.js syntax 23 | - Workaround Tailwind not crashing with non-zero exitcode 24 | 25 | ## 0.1.0.0 26 | 27 | * First version. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sridhar Ratnakumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tailwind 2 | 3 | Run TailwindCSS CLI [without needing](https://srid.ca/nojs) to touch anything JavaScript. No `input.css` or `tailwind.config.js` is necessary. 4 | 5 | ```sh-session 6 | nix run github:srid/tailwind-haskell -- 'src/**/*.hs' -o output.css 7 | ``` 8 | 9 | Compiles CSS classes in the input file paths or patterns, and writes to the output CSS file. Pass `-w` to run in JIT watcher mode. 10 | 11 | ## How to use as Haskell dependency via Nix 12 | 13 | [`pkgs.haskellPackages.tailwind`](https://nixpkgs.haskell.page/p/tailwind) already wraps the necessary runtime dependencies (tailwind with plugins). You may use it along with [the static `which` library](https://github.com/obsidiansystems/which). 14 | 15 | ## Use cases 16 | 17 | This package is used in [Emanote](https://github.com/srid/emanote) to compile the CSS file on the generated website, as well as in other [Ema apps](https://github.com/EmaApps). 18 | -------------------------------------------------------------------------------- /bin/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell ../shell.nix -i bash 3 | set -xe 4 | find src -name \*.hs | xargs ormolu -m inplace 5 | nixpkgs-fmt *.nix 6 | cabal-fmt -i *.cabal -------------------------------------------------------------------------------- /bin/repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | 4 | exec nix develop -c cabal -- repl 5 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | 4 | # This will run ghcid, which uses `./.ghcid` to invoke your program main entry 5 | # point, with the specified args. 6 | # 7 | # If you change ./.ghcid, ghcid will automatically reload. 8 | exec nix develop -c ghcid 9 | -------------------------------------------------------------------------------- /bin/run-via-tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | PROJECT=$(basename `pwd`) 4 | tmux new-session -A -s $PROJECT bin/run 5 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | fetchTarball { 4 | url = "https://github.com/edolstra/flake-compat/archive/12c64ca55c1014cdc1b16ed5a804aa8576601ff2.tar.gz"; 5 | sha256 = "0jm6nzb83wa6ai17ly9fzpqc40wg1viib8klq8lby54agpl213w5"; 6 | } 7 | ) 8 | { 9 | src = ./.; 10 | }).defaultNix 11 | -------------------------------------------------------------------------------- /exe/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE BlockArguments #-} 4 | 5 | module Main where 6 | 7 | import qualified Control.Applicative.Combinators.NonEmpty as NE 8 | import Control.Monad.Logger.CallStack (runStdoutLoggingT) 9 | import Data.Default (Default (def)) 10 | import Main.Utf8 (withUtf8) 11 | import Optics.Core ((%), (.~)) 12 | import Options.Applicative 13 | ( argument, 14 | fullDesc, 15 | help, 16 | info, 17 | long, 18 | metavar, 19 | progDesc, 20 | short, 21 | str, 22 | strOption, 23 | switch, 24 | value, 25 | execParser, 26 | helper, 27 | Parser, option, ReadM, eitherReader, showDefaultWith ) 28 | import System.FilePattern (FilePattern) 29 | import Web.Tailwind 30 | ( Mode(..), 31 | tailwindConfigContent, 32 | tailwindConfig, 33 | tailwindMode, 34 | tailwindOutput, 35 | runTailwind, 36 | Plugin(..), 37 | tailwindConfigPlugins ) 38 | import qualified Text.Megaparsec as Mega 39 | import qualified Text.Megaparsec.Char as Mega 40 | 41 | data Cli = Cli 42 | { content :: NonEmpty FilePattern, 43 | output :: FilePath, 44 | mode :: Mode, 45 | plugins :: [Plugin] 46 | } 47 | -- deriving (Eq, Show) 48 | deriving (Show) 49 | 50 | cliParser :: Parser Cli 51 | cliParser = do 52 | content <- NE.some (argument str (metavar "SOURCES...")) 53 | output <- strOption 54 | ( long "output" 55 | <> short 'o' 56 | <> metavar "OUTPUT" 57 | <> value "tailwind.css" 58 | ) 59 | mode <- modeParser 60 | plugins <- pluginsParser 61 | pure Cli {..} 62 | 63 | modeParser :: Parser Mode 64 | modeParser = do 65 | bool Production JIT <$> switch (long "watch" <> short 'w' <> help "Run JIT") 66 | 67 | pluginsParser :: Parser [Plugin] 68 | pluginsParser = option megaparsecPlugins 69 | ( long "plugins" 70 | <> short 'p' 71 | <> value [Typography, Forms, LineClamp, AspectRatio] 72 | <> showDefaultWith (intercalate "," . fmap show) 73 | <> help "specify enabled plugins, \"\" for none" 74 | ) 75 | where 76 | megaparsecPlugins = megaparsecReader $ Mega.sepBy plugin sep 77 | 78 | sep = Mega.hspace *> Mega.char ',' *> Mega.hspace 79 | plugin = asum $ (\(s, t) -> Mega.string' s $> t) <$> 80 | [ ("typography" , Typography ) 81 | , ("forms" , Forms ) 82 | , ("line-clamp" , LineClamp ) 83 | , ("aspect-ratio", AspectRatio) 84 | ] 85 | 86 | megaparsecReader :: Mega.Parsec Void Text a -> ReadM a 87 | megaparsecReader p = eitherReader 88 | $ first Mega.errorBundlePretty . Mega.parse p "" . toText 89 | 90 | main :: IO () 91 | main = do 92 | withUtf8 $ do 93 | cli <- execParser opts 94 | print cli 95 | runStdoutLoggingT $ 96 | runTailwind $ 97 | def 98 | & tailwindConfig % tailwindConfigContent .~ toList (content cli) 99 | & tailwindConfig % tailwindConfigPlugins .~ plugins cli 100 | & tailwindOutput .~ output cli 101 | & tailwindMode .~ mode cli 102 | where 103 | opts = 104 | info 105 | (cliParser <**> helper) 106 | ( fullDesc 107 | <> progDesc "Run Tailwind" 108 | ) 109 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1738453229, 9 | "narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "haskell-flake": { 22 | "locked": { 23 | "lastModified": 1739713768, 24 | "narHash": "sha256-P8I3uARBkSra86d5lgTo7cK+ET27zVee1BxWrym+9z8=", 25 | "owner": "srid", 26 | "repo": "haskell-flake", 27 | "rev": "b40fcefa008ffa7205058f542a577cc218bec9a4", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "srid", 32 | "repo": "haskell-flake", 33 | "type": "github" 34 | } 35 | }, 36 | "nixpkgs": { 37 | "locked": { 38 | "lastModified": 1739863612, 39 | "narHash": "sha256-UbtgxplOhFcyjBcNbTVO8+HUHAl/WXFDOb6LvqShiZo=", 40 | "owner": "nixos", 41 | "repo": "nixpkgs", 42 | "rev": "632f04521e847173c54fa72973ec6c39a371211c", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "nixos", 47 | "ref": "nixpkgs-unstable", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "nixpkgs-lib": { 53 | "locked": { 54 | "lastModified": 1738452942, 55 | "narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=", 56 | "type": "tarball", 57 | "url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz" 58 | }, 59 | "original": { 60 | "type": "tarball", 61 | "url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz" 62 | } 63 | }, 64 | "root": { 65 | "inputs": { 66 | "flake-parts": "flake-parts", 67 | "haskell-flake": "haskell-flake", 68 | "nixpkgs": "nixpkgs" 69 | } 70 | } 71 | }, 72 | "root": "root", 73 | "version": 7 74 | } 75 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | haskell-flake.url = "github:srid/haskell-flake"; 6 | }; 7 | 8 | outputs = inputs@{ self, nixpkgs, flake-parts, haskell-flake, ... }: 9 | flake-parts.lib.mkFlake { inherit inputs; } { 10 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 11 | 12 | imports = [ 13 | inputs.haskell-flake.flakeModule 14 | ]; 15 | 16 | perSystem = { config, system, pkgs, ... }: 17 | let 18 | tailwindCss = pkgs.nodePackages.tailwindcss.overrideAttrs (oa: { 19 | plugins = [ 20 | pkgs.nodePackages."@tailwindcss/aspect-ratio" 21 | pkgs.nodePackages."@tailwindcss/forms" 22 | pkgs.nodePackages."@tailwindcss/language-server" 23 | pkgs.nodePackages."@tailwindcss/line-clamp" 24 | pkgs.nodePackages."@tailwindcss/typography" 25 | ]; 26 | }); 27 | in 28 | { 29 | # Haskell configuration 30 | haskellProjects.default = { 31 | settings = { 32 | tailwind = { 33 | extraBuildDepends = [ tailwindCss ]; 34 | }; 35 | }; 36 | # Development shell configuration 37 | devShell = { 38 | enable = true; 39 | mkShellArgs = { 40 | nativeBuildInputs = [ 41 | pkgs.nixpkgs-fmt 42 | tailwindCss 43 | ]; 44 | }; 45 | }; 46 | }; 47 | 48 | # Define apps 49 | apps = { 50 | default = { 51 | type = "app"; 52 | program = "${pkgs.writeShellApplication { 53 | name = "tailwind-run.sh"; 54 | text = '' 55 | set -xe 56 | exec ${config.packages.tailwind}/bin/tailwind-run "$@" 57 | ''; 58 | }}/bin/tailwind-run.sh"; 59 | }; 60 | }; 61 | }; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/Web/Tailwind.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE DerivingVia #-} 5 | {-# LANGUAGE QuasiQuotes #-} 6 | {-# LANGUAGE RecordWildCards #-} 7 | {-# LANGUAGE TemplateHaskell #-} 8 | 9 | -- | Tailwind runner in Haskell 10 | -- 11 | module Web.Tailwind 12 | ( -- * Runner 13 | runTailwind, 14 | 15 | -- * Types 16 | TailwindConfig (..), 17 | Tailwind (..), 18 | Mode (..), 19 | Plugin (..), 20 | tailwindConfig, 21 | tailwindInput, 22 | tailwindOutput, 23 | tailwindConfigContent, 24 | tailwindConfigPlugins, 25 | tailwindMode, 26 | ) 27 | where 28 | 29 | import Control.Monad.Logger (MonadLogger, logInfoN) 30 | import Data.ByteString (hPut) 31 | import Data.Default (Default (def)) 32 | import NeatInterpolation (text) 33 | import Optics.TH (makeLenses) 34 | import System.CPUTime (getCPUTime) 35 | import System.Directory (doesFileExist) 36 | import System.IO (hClose) 37 | import System.Which (staticWhich) 38 | import Text.Printf (printf) 39 | import qualified Text.Show 40 | import UnliftIO (MonadUnliftIO, finally) 41 | import UnliftIO.Directory (removeFile) 42 | import UnliftIO.Process (callProcess) 43 | import UnliftIO.Temporary (withSystemTempFile) 44 | import qualified Data.Text as Text 45 | 46 | data Plugin 47 | = Typography 48 | | Forms 49 | | LineClamp 50 | | AspectRatio 51 | 52 | instance Show Plugin where 53 | show = \case 54 | Typography -> "typography" 55 | Forms -> "forms" 56 | LineClamp -> "line-clamp" 57 | AspectRatio -> "aspect-ratio" 58 | 59 | -- | Haskell version of tailwind.config.js 60 | -- 61 | data TailwindConfig = TailwindConfig 62 | { -- | List of source patterns that reference CSS classes 63 | _tailwindConfigContent :: [FilePath], 64 | -- | List of the "official tailwind" plugins, 65 | -- cf. https://tailwindcss.com/docs/plugins 66 | _tailwindConfigPlugins :: [Plugin] 67 | } 68 | deriving (Generic) 69 | 70 | newtype Css = Css {unCss :: Text} 71 | 72 | data Mode = JIT | Production 73 | deriving (Eq, Show) 74 | 75 | data Tailwind = Tailwind 76 | { _tailwindConfig :: TailwindConfig, 77 | _tailwindInput :: Css, 78 | _tailwindOutput :: FilePath, 79 | _tailwindMode :: Mode 80 | } 81 | 82 | makeLenses ''TailwindConfig 83 | makeLenses ''Tailwind 84 | 85 | instance Default TailwindConfig where 86 | def = 87 | TailwindConfig 88 | { _tailwindConfigContent = [] 89 | , _tailwindConfigPlugins = 90 | [ Typography 91 | , Forms 92 | , LineClamp 93 | , AspectRatio 94 | ] 95 | } 96 | 97 | instance Default Tailwind where 98 | def = 99 | Tailwind 100 | { _tailwindConfig = def, 101 | _tailwindInput = def, 102 | _tailwindOutput = "tailwind.css", 103 | _tailwindMode = JIT 104 | } 105 | 106 | instance Default Css where 107 | def = 108 | Css 109 | [text| 110 | @tailwind base; 111 | @tailwind components; 112 | @tailwind utilities; 113 | |] 114 | 115 | instance Text.Show.Show TailwindConfig where 116 | show TailwindConfig{..} = 117 | toString [text| 118 | module.exports = { 119 | content: ${content},${strPlugins} 120 | } 121 | |] 122 | where 123 | content = Text.pack $ show _tailwindConfigContent 124 | strPlugins = if null _tailwindConfigPlugins 125 | then "" 126 | else "\nplugins: [\n " 127 | <> Text.intercalate ",\n " (mkJSStrPlugin <$> _tailwindConfigPlugins) 128 | <> "\n]" 129 | mkJSStrPlugin plugin = 130 | "require('@tailwindcss/" <> Text.pack (show plugin) <> "')" 131 | 132 | tailwind :: FilePath 133 | tailwind = $(staticWhich "tailwindcss") 134 | 135 | modeArgs :: Mode -> [String] 136 | modeArgs = \case 137 | JIT -> ["-w"] 138 | Production -> ["--minify"] 139 | 140 | runTailwind :: (MonadUnliftIO m, MonadLogger m, HasCallStack) => Tailwind -> m () 141 | runTailwind Tailwind {..} = do 142 | withTmpFile (show _tailwindConfig) $ \configFile -> 143 | withTmpFile (unCss _tailwindInput) $ \inputFile -> 144 | let f = bool id (failIfFileNotCreated _tailwindOutput) (_tailwindMode == Production) 145 | in f $ callTailwind $ ["-c", configFile, "-i", inputFile, "-o", _tailwindOutput] <> modeArgs _tailwindMode 146 | when (_tailwindMode == JIT) $ 147 | error "Tailwind exited unexpectedly!" 148 | 149 | withTmpFile :: MonadUnliftIO m => Text -> (FilePath -> m a) -> m a 150 | withTmpFile s f = do 151 | withSystemTempFile "ema-tailwind-tmpfile" $ \fp h -> do 152 | liftIO $ do 153 | putStrLn $ "$ cat " <> fp 154 | putTextLn s 155 | hPut h (encodeUtf8 s) >> hClose h 156 | f fp 157 | `finally` removeFile fp 158 | 159 | -- Workaround for https://github.com/srid/emanote/issues/232 160 | -- 161 | -- If the given IO action doesnot create this file (we remove the file before 162 | -- running the IO action), then fail with `error`. 163 | failIfFileNotCreated :: (MonadUnliftIO m, HasCallStack) => FilePath -> m a -> m a 164 | failIfFileNotCreated fp m = do 165 | liftIO (doesFileExist fp) >>= \case 166 | True -> removeFile fp 167 | _ -> pure () 168 | x <- m 169 | exists <- liftIO $ doesFileExist fp 170 | if exists 171 | then pure x 172 | else error $ "File not created: " <> toText fp 173 | 174 | callTailwind :: (MonadIO m, MonadLogger m) => [String] -> m () 175 | callTailwind args = do 176 | logInfoN $ "Running Tailwind at " <> toText tailwind <> " with args: " <> show args 177 | liftIO (doesFileExist tailwind) >>= \case 178 | True -> 179 | timeIt $ do 180 | callProcess tailwind args 181 | False -> 182 | error $ "Tailwind compiler not found at " <> toText tailwind 183 | 184 | timeIt :: MonadIO m => m b -> m b 185 | timeIt m = do 186 | t0 <- liftIO getCPUTime 187 | !x <- m 188 | t1 <- liftIO getCPUTime 189 | let diff :: Double = fromIntegral (t1 - t0) / (10 ^ (9 :: Integer)) 190 | liftIO $ printf "Process duration: %0.3f ms\n" diff 191 | pure $! x 192 | -------------------------------------------------------------------------------- /tailwind.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: tailwind 3 | version: 0.4.0.0 4 | license: MIT 5 | copyright: 2022 Sridhar Ratnakumar 6 | maintainer: srid@srid.ca 7 | author: Sridhar Ratnakumar 8 | category: Web 9 | 10 | -- TODO: Before hackage release. 11 | -- A short (one-line) description of the package. 12 | synopsis: Tailwind wrapped in Haskell 13 | 14 | -- A longer description of the package. 15 | description: Run Tailwind from Haskell without touching JavaScript 16 | 17 | -- A URL where users can report bugs. 18 | bug-reports: https://github.com/srid/tailwind-haskell 19 | extra-source-files: 20 | CHANGELOG.md 21 | LICENSE 22 | README.md 23 | 24 | common c 25 | mixins: 26 | base hiding (Prelude), 27 | relude (Relude as Prelude, Relude.Container.One), 28 | relude 29 | 30 | ghc-options: 31 | -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns 32 | 33 | build-depends: 34 | , base >=4.13.0.0 && <=4.99.0.0 35 | , data-default 36 | , filepath 37 | , filepattern 38 | , monad-logger 39 | , optics-th 40 | , relude 41 | 42 | default-extensions: 43 | FlexibleContexts 44 | FlexibleInstances 45 | KindSignatures 46 | LambdaCase 47 | MultiParamTypeClasses 48 | MultiWayIf 49 | OverloadedStrings 50 | ScopedTypeVariables 51 | TupleSections 52 | ViewPatterns 53 | 54 | default-language: Haskell2010 55 | 56 | library 57 | import: c 58 | hs-source-dirs: src 59 | exposed-modules: Web.Tailwind 60 | build-depends: 61 | async 62 | , bytestring 63 | , containers 64 | , directory 65 | , mtl 66 | , neat-interpolation 67 | , profunctors 68 | , safe-exceptions 69 | , temporary 70 | , text 71 | , time 72 | , unliftio 73 | , which 74 | , with-utf8 75 | 76 | executable tailwind-run 77 | import: c 78 | hs-source-dirs: exe 79 | main-is: Main.hs 80 | build-depends: 81 | megaparsec 82 | , optics-core 83 | , optparse-applicative 84 | , parser-combinators 85 | , tailwind 86 | , with-utf8 87 | 88 | test-suite tests 89 | import: c 90 | main-is: Main.hs 91 | type: exitcode-stdio-1.0 92 | hs-source-dirs: tests 93 | build-depends: 94 | hspec 95 | , neat-interpolation 96 | , optics-core 97 | , tailwind 98 | -------------------------------------------------------------------------------- /tests/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | 4 | module Main where 5 | 6 | import Test.Hspec (describe, hspec, it, shouldBe) 7 | import Web.Tailwind 8 | import Data.Default (Default(def)) 9 | import NeatInterpolation (text) 10 | import Optics.Core ((.~)) 11 | 12 | strDefaultConfig :: Text 13 | strDefaultConfig = [text| 14 | module.exports = { 15 | content: [], 16 | plugins: [ 17 | require('@tailwindcss/typography'), 18 | require('@tailwindcss/forms'), 19 | require('@tailwindcss/line-clamp'), 20 | require('@tailwindcss/aspect-ratio') 21 | ] 22 | } 23 | |] 24 | 25 | strConfigNoPlugins :: Text 26 | strConfigNoPlugins = [text| 27 | module.exports = { 28 | content: [], 29 | } 30 | |] 31 | 32 | strConfigSomePlugins :: Text 33 | strConfigSomePlugins = [text| 34 | module.exports = { 35 | content: [], 36 | plugins: [ 37 | require('@tailwindcss/aspect-ratio'), 38 | require('@tailwindcss/forms'), 39 | require('@tailwindcss/typography') 40 | ] 41 | } 42 | |] 43 | 44 | main :: IO () 45 | main = hspec $ do 46 | describe "TailwindConfig" do 47 | it "matches the default .js file content" $ 48 | show (def :: TailwindConfig) `shouldBe` strDefaultConfig 49 | it "matches the .js file content w/o plugins" $ 50 | show (def & tailwindConfigPlugins .~ []) `shouldBe` strConfigNoPlugins 51 | it "matches the .js file content w/ some plugins" $ 52 | show (def & tailwindConfigPlugins .~ [AspectRatio, Forms, Typography]) 53 | `shouldBe` strConfigSomePlugins 54 | --------------------------------------------------------------------------------