This repository provides the pyaco
binary, a Node.js libary, and a Rust crate to help you deal with css stylesheet in a type safe way.
Generates code from any valid css file (this CLI has been tested against complex CSS files generated by Tailwind). Currently supports TypeScript, ReScript, Elm, and PureScript (Rust users: you can see below how to use the css!
macro).
Using cargo
:
cargo install --git https://github.com/scoville/tailwind-generator
Using npm
/yarn
:
npm install https://github.com/scoville/tailwind-generator
or
yarn add https://github.com/scoville/tailwind-generator
Not all platforms are currently supported officially (MacOS M1 / AArch64 for instance).
Nonetheless, for the time being and in order to make this tool as easy as possible to use, when a platform is not recognized the node native "binary" plus an alternative CLI facade in Node.js will be used instead. So all platforms that support Node should work now. Notice that a small performance degradation is to be expected.
To get help:
pyaco generate --help
pyaco-generate
Generate code from a css input
USAGE:
pyaco generate [OPTIONS] --input <INPUT> --output-filename <OUTPUT_FILENAME> --lang <LANG>
OPTIONS:
-f, --output-filename <OUTPUT_FILENAME>
Filename (without extension) used for the generated code
-h, --help
Print help information
-i, --input <INPUT>
CSS file path and/or URL to parse and generate code from
-l, --lang <LANG>
Language used in generated code (elm|purescript|rescript|typescript|typescript-type-
1|typescript-type-2)
-o, --output-directory <OUTPUT_DIRECTORY>
Directory for generated code [default: ./]
-w, --watch
Watch for changes in the provided css file and regenarate the code (doesn't work with
URL)
--watch-debounce-duration <WATCH_DEBOUNCE_DURATION>
Watch debounce duration (in ms), if files are validated twice after saving the css file,
you should try to increase this value [default: 10]
Warning: the -w|--watch
mode is still experimental and might contain some bugs, use with care.
pyaco generate
uses env_logger under the hood, so you can prefix your command with RUST_LOG=info
for a more verbose output, the binary is silent by default.
Warning: in PureScript and Elm, the provided filename and directory path will be used as the module name, make sure they follow the name conventions and are capitalized. For example:
pyaco generate -i ./styles.css -l purescript -o ./Foo/Bar -f Baz
Will generate a ./Foo/Bar/Baz.purs
file that defines a module called Foo.Bar.Baz
.
Display the help message:
pyaco generate -h
Generates a TypeScript file called css.ts
in the generated
folder from the Tailwind CDN:
pyaco generate \
-i https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css \
-l typescript \
-f css \
-o generated
Same as above but generated from a local file:
pyaco generate \
-i ./styles.css \
-l typescript \
-f css \
-o generated
Same as above and regenerate code on CSS file change:
pyaco generate \
-i ./styles.css \
-l typescript \
-f css \
-o generated \
-w
Generates a PureScript file and displays logs:
RUST_LOG=info pyaco generate \
-i ./styles.css \
-l purescript \
-f Css
pyaco generate
offers three flavors for TypeScript code generation, let's see and compare the three solutions.
A simple generator for TypeScript, it exports an opaque type CssClass
, a join
function, and a set of CssClass
"objects":
import { join, textBlue100, rounded, border, borderBlue300 } from "./css.ts";
// ...
<div className={join([textBlue100, rounded, border, borderBlue300])}>
Hello
</div>;
Pros:
- Easy to use
- Very flexible
- Compatible with most TypeScript versions
- Safe, you can't pass any string to the
join
function - Autocompletion
Cons:
- Cost at runtime:
CssClass
are JavaScript objects that help ensuring type opacity - Cost at runtime: the array has to be joined into a string
- Imports can be verbose (unless you use
import * as ...
) - Not the "standard" class names,
h-full
becomeshFull
, etc...
This generator doesn't generate any runtime code apart from the join
function.
import { join } from "./css.ts";
// ...
<div className={join("text-blue-100", "rounded", "border", "border-blue-300")}>
Hello
</div>;
Pros:
- Easy to use
- Very flexible
- Compatible with most TypeScript versions
- Safe, you can't pass any string to the
tailwind
function - "Standard" class names
- Light import (you only need the
join
function) - Autocompletion
Cons:
- Cost at runtime: the classes must be "joined" into a string
This generator doesn't generate any runtime code apart from the css
function.
import { css } from "./css.ts";
// ...
<div className={css("text-blue-100 rounded border border-blue-300")}>
Hello
</div>;
Pros:
- Super easy to use
- Safe, you can't pass any string to the
tailwind
function - "Standard" class names
- Light import (you only need the
css
function) - No runtime cost at all
- Partial support for autocompletion
Cons:
- Not as flexible as the 2 other generators
- Compatible with TypeScript > 4.1 only
- Type error can be hard to debug
- Doesn't accept multiple spaces (not necessarily a cons for some)
In PureScript, a CssClass
newtype is exported without its constructor which derives some very useful type classes like Semigroup
or Monoid
offering a lot of flexibility:
- Simple list of css classes:
[ rounded, borderRed100 ]
- Add a class conditionally:
[ if true then textBlue500 else textRed500 ] -- "text-blue-500"
- Add a class only if a condition is met, do nothing otherwise:
[ guard true textBlue500 ] -- "text-blue-500"
[ guard false rounded ] -- ""
- Handle Maybe, and other
Foldable
values:
[ rounded, fold Nothing ] -- "rounded"
[ rounded, fold $ Right wFull ] -- "rounded w-full"
let mClass = Just borderRed100 in
[ rounded, fold mClass ] -- "rounded border-red-100"
Example:
import Css (rounded, borderRed100, join)
css :: String
css = join [ rounded, borderRed100 ]
You can also take a look at this ppx if you want to skip the code generation step. Both approach (code generation and ppx) have pros and cons.
The ppx got deprecated.
In ReScript, 2 files are generated one that contains the code and an interface file.
Additionally to the class variables, 2 functions are exposed:
join
: takes a list ofcssClass
and returns a stringjoinOpt
: takes a list ofoption<cssClass>
and returns a string
open Css
<div className={join([textBlue100, rounded, border, borderBlue300])}>
{"Hello!"->React.string}
</div>
Since ReScript 9.1 we can safely coerce polymorphic variants to strings. This generator leverages this new feature.
It's lighter than the other ReScript generator, and it's possible to get class names autocompletion using the Tailwind IntelliSense plugin.
Example:
<div className={Css.join([#"text-blue-100", #rounded, #border, #"border-blue-300"])}>
{"Hello!"->React.string}
</div>
Additionally to the generated classes, you'll get 2 useful functions:
classes
: takes a list of css classes and returns anHtml.Attribute msg
that can be used with any html elementjoin
: performs a simpleList CssClass -> String
conversion when you need to compute a class name outside of an html element
import Css exposing (classes, textBlue100, rounded, border, borderBlue300);
view _model =
div [ classes [ textBlue100, rounded, border, borderBlue300 ] ]
[ text "Hello!" ]
Some languages allow for more flexibility using macros or another mechanism. Rust, Crystal, or the OCaml languages (Ocaml, ReasonML, and ReScript) are some of these languages, and pyaco
offers support for some of them.
ReScript users: this tool doesn't offer any other support than the generator (see above) yet, in the meantime you can take a look at this ppx.
In Rust, a pyaco.toml
file is required and must be located at the root of your crate. Its content is pretty simple (as of today) and should look like this:
[general]
input = "./styles.css" # or input = {path = "./styles.css"}
Notice that urls are also supported, which can come in handy when testing or developing your application as in that case no files are required:
[general]
input = {url = "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"}
If your config file is valid and the css can be found, you can now use the css!
macro:
use pyaco_macro::css;
// ...
let style = css!(" rounded border px-2 py-1");
// Notice that extra white spaces have been removed at compile time
assert_eq!(style, "rounded border px-2 py-1");
The css class names are validated and cleaned at compile time, duplicates are removed (a compiler warning is emitted if you use Rust nightly) and the whole macro call is replaced by the provided string itself.
Yew users: the css!
macro can be used instead of the classes!
one.
The pyaco validate
command will take a css input (path or URL) and a glob of files to validate. If a class is used in a file but not present in the css input an error is displayed.
pyaco validate
will not force you to change your workflow, nor will it generate files in your project. It's not a macro/ppx either.
Put simply, it's a specialized grep
that will read all the files you want to validate, check for the css class names, and exit. Since it's fast (less than 2 seconds to analyze more than 5000 files that all contained more than 600 lines of code on my pretty old machine, not even half a second on ~500 files projects), it can be integrated easily into your favorite CI tool.
This binary is still experimental as we need to test it out more on larger codebase in TypeScript first, also some much needed quality of life improvements are still being worked on (watch mode, whitelist, configuration file, etc...).
To get help:
pyaco validate --help
pyaco-validate
Validate code against a css input
USAGE:
pyaco validate [OPTIONS] --css-input <CSS_INPUT> --input-glob <INPUT_GLOB>
OPTIONS:
-c, --css-input <CSS_INPUT>
CSS file path or URL used for code verification
--capture-regex <CAPTURE_REGEX>
Classes matcher regex, must include a capture containing all the classes [default:
class="([^"]+)"]
-h, --help
Print help information
-i, --input-glob <INPUT_GLOB>
Glob pointing to the files to validate
--max-opened-files <MAX_OPENED_FILES>
How many files can be read concurrently at most, setting this value to a big number
might break depending on your system [default: 128]
--split-regex <SPLIT_REGEX>
Classes splitter regex, will split the string captured with the `capture_regex` argument
and split it into classes [default: \s+]
-w, --watch
Watch for changes in the provided css file and files and revalidate the code (doesn't
work with URL)
--watch-debounce-duration <WATCH_DEBOUNCE_DURATION>
Watch debounce duration (in ms), if files are validated twice after saving a file, you
should try to increase this value [default: 10]
Warning: the -w|--watch
mode is still experimental and might contain some bugs, use with care.
The API is very likely to change soon, please use with care.
When installed with npm
or yarn
you can execute the provided cli or alternatively use pyaco
just like any Node.js module:
import { generate, validate } from "pyaco";
generate({
input: "...",
lang: "purescript",
outputDirectory: "...",
watch: false,
watchDebounceDuration: 0,
outputFilename: "...",
});
validate({
cssInput: "...",
inputGlob: "...",
captureRegex: "...",
maxOpenedFiles: 128,
splitRegex: "...",
watch: false,
watchDebounceDuration: 0,
}).then(() => {
console.log("Done");
});