├── .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 |
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 |
--------------------------------------------------------------------------------