├── .tool-versions ├── .npmignore ├── demo ├── .tool-versions ├── Additional.css ├── Sheet.css ├── tailwind.config.js ├── README.md ├── App.html ├── package.json ├── App.elm ├── elm.json └── Makefile ├── .gitignore ├── package.json ├── README.md ├── LICENSE ├── CHANGELOG.md ├── css-classes.js └── cli.js /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 14.2.0 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | .tool-versions 3 | -------------------------------------------------------------------------------- /demo/.tool-versions: -------------------------------------------------------------------------------- 1 | elm 0.19.1 2 | -------------------------------------------------------------------------------- /demo/Additional.css: -------------------------------------------------------------------------------- 1 | .additional { 2 | .nested { 3 | margin: 1px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /demo/build 2 | /demo/elm-stuff 3 | /demo/package-lock.json 4 | /demo/Tailwind.elm 5 | node_modules 6 | -------------------------------------------------------------------------------- /demo/Sheet.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | @import "./Additional.css"; 6 | -------------------------------------------------------------------------------- /demo/tailwind.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | theme: { 4 | inset: { 5 | "1/2": "50%", 6 | }, 7 | extend: { 8 | screens: { 9 | dark: { raw: '(prefers-color-scheme: dark)' } 10 | } 11 | } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | _Few things:_ 2 | - This uses Node v14.x 3 | - This uses ES6 modules in Node by setting `"type": "module"` in the `package.json` file 4 | - You can test this by running `npm install` and then `make` 5 | - Elm app will fail to compile if that selector doesn't exist 6 | -------------------------------------------------------------------------------- /demo/App.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "engines": { 5 | "node": ">=14" 6 | }, 7 | "dependencies": { 8 | "elm-tailwind-css": "file:../", 9 | "postcss": "^8.2.1", 10 | "postcss-import": "^13.0.0", 11 | "postcss-nesting": "^7.0.1", 12 | "tailwindcss": "2.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo/App.elm: -------------------------------------------------------------------------------- 1 | module App exposing (..) 2 | 3 | import Browser 4 | import Html 5 | import Tailwind as T 6 | 7 | 8 | main : Program () () () 9 | main = 10 | Browser.sandbox 11 | { init = () 12 | , update = \_ _ -> () 13 | , view = view 14 | } 15 | 16 | 17 | view _ = 18 | Html.div 19 | [ T.dark__bg_black 20 | , T.translate_x_1over2 21 | ] 22 | [] 23 | -------------------------------------------------------------------------------- /demo/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "." 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0" 12 | }, 13 | "indirect": { 14 | "elm/json": "1.1.3", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @rm -rf build 3 | 4 | @npx etc Sheet.css \ 5 | --config tailwind.config.js \ 6 | --elm-path Tailwind.elm \ 7 | --output build/sheet.css \ 8 | \ 9 | --post-plugin-before postcss-import \ 10 | --post-plugin-after postcss-nested 11 | 12 | # @elm make App.elm --output build/app.js 13 | 14 | @NODE_ENV=production npx etc Sheet.css \ 15 | --config tailwind.config.js \ 16 | --output build/sheet.css \ 17 | \ 18 | --post-plugin-before postcss-import \ 19 | --post-plugin-after postcss-nested \ 20 | \ 21 | --purge-content ./App.html \ 22 | --purge-content ./build/app.js 23 | 24 | # Check if build output is correct 25 | @cat build/sheet.css 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-tailwind-css", 3 | "description": "Use Tailwind CSS with Elm", 4 | "version": "1.1.1", 5 | "keywords": [ 6 | "tailwind", 7 | "elm", 8 | "purgecss", 9 | "postcss", 10 | "minify" 11 | ], 12 | "homepage": "https://github.com/icidasset/elm-tailwind-css", 13 | "repository": "https://github.com/icidasset/elm-tailwind-css", 14 | "license": "MIT", 15 | "author": "Steven Vandevelde ", 16 | "type": "module", 17 | "bin": { 18 | "etc": "cli.js" 19 | }, 20 | "peerDependencies": { 21 | "tailwindcss": ">= 0.0" 22 | }, 23 | "dependencies": { 24 | "@fullhuman/postcss-purgecss": "3.1.0-alpha.0", 25 | "autoprefixer": ">= 9.0", 26 | "cssnano": "^4.1.10", 27 | "fast-stable-stringify": "^1.0.0", 28 | "hash-wasm": "^4.4.1", 29 | "meow": "^8.0", 30 | "postcss": "^8.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🌳 __Use Tailwind CSS with Elm.__ 2 | 3 | _Generates an Elm module with functions for all your CSS selectors 4 | and the ones from Tailwind. For the production build it filters 5 | out all the unused selectors and minifies the css file._ 6 | 7 | In other words, pretty much a CLI for [monty5811/postcss-elm-tailwind](https://github.com/monty5811/postcss-elm-tailwind) 8 | and [FullHuman/purgecss](https://github.com/FullHuman/purgecss), plus CSS minifying. 9 | 10 | ## Usage 11 | 12 | ```shell 13 | npm install elm-tailwind-css --save-dev 14 | npx etc --help 15 | 16 | # Make a CSS build with all the Tailwind stuff 17 | # and generate the Elm module 18 | npx etc Sheet.css 19 | --config tailwind.config.js 20 | --elm-path Tailwind.elm 21 | --output build/sheet.css 22 | 23 | # Make a minified & purged CSS build 24 | NODE_ENV=production npx etc Sheet.css 25 | --config tailwind.config.js 26 | --output build/sheet.css 27 | 28 | --purge-content ./build/**/*.html 29 | --purge-content ./build/app.js 30 | ``` 31 | 32 | See the `demo` directory in this repo for more details. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Vandevelde 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.1.0 2 | 3 | - Tailwind v2 support 4 | - whitelist → safelist 5 | - replace `csso` with `cssnano` 6 | 7 | 8 | ### 1.0.0 9 | 10 | Use custom PostCSS plugin to generate the Elm file. 11 | This will only generate the Elm files if there are changes. 12 | 13 | 14 | ### 0.4.8 15 | 16 | Allow every version of Tailwind. 17 | 18 | 19 | ### 0.4.7 20 | 21 | Remove node engine requirement. 22 | 23 | 24 | ### 0.4.6 25 | 26 | Remove tailwind dependency, use tailwind from project. 27 | 28 | 29 | ### 0.4.5 30 | 31 | Remove `--no-warning` in binary. 32 | 33 | 34 | ### 0.4.4 35 | 36 | Fix incorrect dependency commit hash. 37 | 38 | 39 | ### 0.4.3 40 | 41 | Update Tailwind. 42 | 43 | 44 | ### 0.4.2 45 | 46 | - Adds ability to choose the Elm module name 47 | - Show help when given no parameters 48 | 49 | 50 | ### 0.4.1 51 | 52 | Temporarily use fork of `postcss-elm-tailwind` that removes the `:not()` pseudo classes. 53 | 54 | 55 | ### 0.4.0 56 | 57 | - Add ability to use additional PostCSS plugins 58 | - Make the Elm module optional 59 | 60 | 61 | ### 0.3.0 62 | 63 | Update `tailwindcss` to v1.5 64 | 65 | 66 | ### 0.2.0 67 | 68 | - Use same `defaultExtractor` for PurgeCSS as Tailwind 69 | - Update dependencies 70 | -------------------------------------------------------------------------------- /css-classes.js: -------------------------------------------------------------------------------- 1 | import hashwasm from "hash-wasm" 2 | import fs from "fs" 3 | import postcss from "postcss" 4 | import stringify from "fast-stable-stringify" 5 | 6 | 7 | // This'll generate an Elm module with a function for each CSS class we have. 8 | // It will also generate a "CSS table" with the "css_class <=> elm_function" relation. 9 | // This "CSS table" makes it possible to only keep the CSS that's actually used. 10 | const plugin = async (flags, root, result) => { 11 | 12 | const functions = [] 13 | const lookup = {} 14 | 15 | const processSelector = (selector, rule) => { 16 | if (!selector.startsWith(".")) return 17 | 18 | const cls = selector 19 | .replace(/^\./, "") 20 | .replace(/\\\./g, ".") 21 | .replace(/\s?>\s?.*/, "") 22 | .replace(/::.*$/, "") 23 | .replace(/:not\([^\)]*\)/g, "") 24 | .replace( 25 | /(:(active|after|before|checked|disabled|focus|focus-within|hover|visited|nth-child\((even|odd)\)|(first|last)-child))+$/, 26 | "" 27 | ) 28 | .replace(/\\\//g, "/") 29 | .replace(/\\([/])/g, "\\\\$1") 30 | .replace(/\\([:])/g, "$1") 31 | .replace(/(^\s+)|(\s+$)/g, "") 32 | .replace(/^\\32/, "") 33 | 34 | const elmVariable = cls 35 | .replace(/:/g, "__") 36 | .replace(/__-/g, "__neg_") 37 | .replace(/^-/g, "neg_") 38 | .replace(/-/g, "_") 39 | .replace(/\./g, "_") 40 | .replace(/\//g, "over") 41 | 42 | const elmVarWithProperCase = flags.elmNameStyle === "camel" 43 | ? elmVariable.replace(/(_+\w)/g, g => g.replace(/_/g, "").toUpperCase()) 44 | : elmVariable 45 | 46 | if (lookup[elmVarWithProperCase]) return 47 | 48 | const css = rule 49 | .toString() 50 | .replace(/\s+/g, " ") 51 | .replace(/(\w)\{/g, "$1 {") 52 | 53 | functions.push( 54 | `{-| This represents the \`.${cls}\` class.\n` + 55 | `\n ${css}` + 56 | `\n-}\n` + 57 | `${elmVarWithProperCase} : Html.Attribute msg\n` + 58 | `${elmVarWithProperCase} = A.class "${cls}"\n` 59 | ) 60 | 61 | lookup[elmVarWithProperCase] = cls 62 | } 63 | 64 | root.walkRules(rule => { 65 | [].concat(...rule.selector.split(",").map(a => a.split(" "))) 66 | .forEach(s => processSelector(s, rule)) 67 | }) 68 | 69 | const tmpDir = `./elm-stuff/elm-tailwind-css/${flags.elmModule}` 70 | fs.mkdirSync(tmpDir, { recursive: true }) 71 | 72 | const header = `module ${flags.elmModule} exposing (..)\n\n` 73 | const imports = [ "import Html", "import Html.Attributes as A" ] 74 | const contents = header + imports.join("\n") + "\n\n" + functions.join("\n\n") 75 | const table = stringify(lookup) 76 | const hash = await hashwasm.xxhash32(table, 1) 77 | const previousHash = fs.readFileSync(`${tmpDir}/css-table.cache`, { flag: "a+", encoding: "utf-8" }) 78 | 79 | if (hash === previousHash) return; 80 | 81 | fs.writeFileSync(`${tmpDir}/css-table.cache`, hash) 82 | fs.writeFileSync(`${tmpDir}/css-table.json`, table) 83 | fs.writeFileSync(flags.elmPath, contents) 84 | 85 | } 86 | 87 | 88 | export default flags => ({ 89 | postcssPlugin: "elm-css-classes", 90 | Once (root, { result }) { return plugin(flags, root, result) } 91 | }) 92 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | import { createRequire } from "module" 5 | import autoprefixer from "autoprefixer" 6 | import cssClasses from "./css-classes.js" 7 | import cssnano from "cssnano" 8 | import fs from "fs" 9 | import path from "path" 10 | import process from "process" 11 | import postcss from "postcss" 12 | import purgecss from "@fullhuman/postcss-purgecss" 13 | 14 | const cwd = path.resolve(".") 15 | const isProduction = (process.env.NODE_ENV === "production") 16 | const meow = createRequire(import.meta.url)("meow") 17 | const requireInProject = createRequire(path.join(cwd, "node_modules")) 18 | 19 | 20 | // ⌨️ 21 | 22 | 23 | const cli = meow(` 24 | Usage 25 | $ etc 26 | 27 | Options 28 | --config, -c Provide a Tailwind configuration file 29 | --output, -o Output path (default: build/stylesheet.css) 30 | 31 | 🌳 Elm Options 32 | 33 | https://github.com/monty5811/postcss-elm-tailwind 34 | 35 | --elm-module, -m Module name for the generated Elm file (default is file name) 36 | --elm-name-style, -n Naming style for Elm functions, "snake" (default) or "camel" 37 | --elm-path, -e Path for the generated Elm Tailwind file 38 | 39 | 🚽 PurgeCSS Options 40 | 41 | https://purgecss.com/CLI.html 42 | You can add these flags multiple times: 43 | 44 | --purge-content [REQUIRED*] Glob that should be analyzed by PurgeCSS 45 | --purge-safelist CSS selector not to be removed by PurgeCSS 46 | 47 | * Only when used with NODE_ENV=production 48 | 49 | ⚗️ PostCSS Options 50 | 51 | https://postcss.org/ 52 | You can add these flags multiple times: 53 | 54 | --post-plugin-before Name of a plugin to set up before the tailwindcss plugin 55 | --post-plugin-after Name of a plugin to set up after Tailwind and before auto-prefixer 56 | 57 | Examples 58 | $ etc src/stylesheet.css 59 | --config tailwind.config.js 60 | --elm-path src/Library/Tailwind.elm 61 | --output build/stylesheet.css 62 | 63 | $ NODE_ENV=production etc src/stylesheet.css 64 | --config tailwind.config.js 65 | --output build/stylesheet.css 66 | --purge-content build/elmApp.js 67 | `, { 68 | flags: { 69 | config: { 70 | type: "string", 71 | alias: "c" 72 | }, 73 | elmModuleName: { 74 | type: "string", 75 | alias: "m" 76 | }, 77 | elmNameStyle: { 78 | type: "string", 79 | alias: "n", 80 | default: "snake" 81 | }, 82 | elmPath: { 83 | type: "string", 84 | alias: "e" 85 | }, 86 | output: { 87 | type: "string", 88 | alias: "o", 89 | default: "build/stylesheet.css" 90 | }, 91 | postPluginAfter: { 92 | type: "string", 93 | isMultiple: true 94 | }, 95 | postPluginBefore: { 96 | type: "string", 97 | isMultiple: true 98 | }, 99 | purgeContent: { 100 | type: "string", 101 | isMultiple: true, 102 | isRequired: isProduction 103 | }, 104 | purgeSafelist: { 105 | type: "string", 106 | isMultiple: true 107 | } 108 | } 109 | }) 110 | 111 | 112 | 113 | // 🏔 114 | 115 | 116 | const tailwindConfigPromise = ( async () => { 117 | if (cli.flags.config) { 118 | const { default: config } = await import( 119 | path.join(process.env.PWD, cli.flags.config) 120 | ) 121 | 122 | return config 123 | 124 | } else { 125 | return undefined 126 | 127 | } 128 | })() 129 | 130 | const input = cli.input[0] 131 | const output = cli.flags.output 132 | 133 | 134 | 135 | // CHECK INPUT 136 | 137 | 138 | if (!input) cli.showHelp() 139 | 140 | 141 | 142 | // FLOW 143 | 144 | 145 | const flow = async maybeTailwindConfig => [ 146 | 147 | // Plugins 148 | ...(await loadPlugins(cli.flags.postPluginBefore)), 149 | 150 | // Tailwind 151 | (await tailwind())({ ...(maybeTailwindConfig || {}), purge: false }), 152 | 153 | // Generate Elm module based on our Tailwind configuration 154 | // OR: make CSS as small as possible by removing style rules we don't need 155 | ...isProduction 156 | 157 | ? [ 158 | 159 | purgecss({ 160 | content: [...cli.flags.purgeContent], 161 | safelist: [...cli.flags.purgeSafelist], 162 | 163 | // Taken from Tailwind src 164 | // https://github.com/tailwindcss/tailwindcss/blob/61ab9e32a353a47cbc36df87674702a0a622fa96/src/lib/purgeUnusedStyles.js#L84 165 | defaultExtractor: content => { 166 | const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [] 167 | const broadMatchesWithoutTrailingSlash = broadMatches.map(m => m.replace(/\\+$/, "")) 168 | const innerMatches = content.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || [] 169 | return broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches) 170 | } 171 | }) 172 | 173 | ] 174 | 175 | : ( 176 | 177 | cli.flags.elmPath 178 | 179 | ? [ 180 | 181 | cssClasses({ 182 | ...cli.flags, 183 | elmModule: ( 184 | cli.flags.elmModule || 185 | cli.flags.elmPath.split(path.sep).slice(-1)[0].replace(/\.\w+$/, "") 186 | ) 187 | }) 188 | 189 | ] 190 | 191 | : [] 192 | 193 | ), 194 | 195 | // Plugins 196 | ...(await loadPlugins(cli.flags.postPluginAfter)), 197 | 198 | // Add vendor prefixes where necessary 199 | autoprefixer, 200 | 201 | // Minify CSS if needed 202 | ...( 203 | 204 | isProduction 205 | 206 | ? [ cssnano({ 207 | preset: [ 208 | "default", 209 | { discardComments: { removeAll: true }} 210 | ] 211 | }) 212 | ] 213 | 214 | : [] 215 | 216 | ) 217 | 218 | ] 219 | 220 | 221 | function loadPlugins(list) { 222 | return Promise.all((list || []).map( 223 | async a => await requireInProject(a) 224 | )) 225 | } 226 | 227 | 228 | function tailwind() { 229 | return requireInProject("tailwindcss") 230 | } 231 | 232 | 233 | 234 | // BUILD 235 | 236 | 237 | tailwindConfigPromise.then(async maybeTailwindConfig => { 238 | fs.mkdirSync( 239 | output.split(path.sep).slice(0, -1).join(path.sep), 240 | { recursive: true } 241 | ) 242 | 243 | const css = fs.readFileSync(input) 244 | const cfg = await flow(maybeTailwindConfig) 245 | const res = await postcss(cfg).process(css, { from: input, to: output }) 246 | 247 | fs.writeFileSync(output, res.css) 248 | }) 249 | --------------------------------------------------------------------------------