Skip to content

Latest commit

 

History

History
418 lines (282 loc) · 13.4 KB

README.md

File metadata and controls

418 lines (282 loc) · 13.4 KB

Pyaco (Polyglot tYpe sAfe Css tOolbox)

For the old version of this application, please see here.

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.

Pyaco generate: a type safe CSS to "*" code generator, tailored for Tailwind

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).

Installation

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

Unsupported platforms

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.

Check the /npm folder and the package.json at the main path to get a better undertanding of how the package is being installed and run. Long story short, it uses Neon, a toolchain which allows creating a native Node module from the rust code. Behind the scenes, the node js implementation will call the Rust code that you can find in crates such as pyaco-validate.

Running the tool in dev mode

You'll find the detailed explanation on how to run the compiled binary in the sections below. In order to run it in dev mode you'll have to replace pyaco with cargo run. Also, to get more logs in dev mode you need to add RUST_LOG=info. So in dev mode, the pyaco validate command would look like this:

RUST_LOG=info cargo run validate -c .styles.css -i './src/**/*.tsx' --capture-regex 'Cn\.c\(\s*\n?"([^"]*)",?\n?\s*\)'

Building the rust binary:

  • Run cargo build --release for compiling the binaries on your architecture

  • Build for a specific architecture, e.g. for Mac x86 if you are on an arm64 Mac

rustup target add x86_64-apple-darwin
cargo build --release --target x86_64-apple-darwin
  • In order to build the binaries cross-platform you need to have an understanding of how a makefile works. Please note there is a Makefile in the main path of this repository. The command to run is make release; this will compile the node.js bindings with Neon and build the app cross platform using cargo for Mac and cross for Windows and Linux.

Commands:

To get help:

pyaco generate --help
pyaco-generate
Generate code from a css input

USAGE:
    pyaco generate [FLAGS] [OPTIONS] --input <input> --output-filename <output-filename> --lang <lang>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information
    -w, --watch      Watch for changes in the provided css file and regenarate the code (doesn't
                     work with URL)

OPTIONS:
    -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: ./]
    -f, --output-filename <output-filename>
            Filename (without extension) used for the generated code

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.

Examples

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

Warning: the -w|--watch mode is still experimental and might contain some bugs (for instance the file is sometimes generated twice), use with care.

Generators

TypeScript

pyaco generate offers three flavors for TypeScript code generation, let's see and compare the three solutions.

TypeScript (typescript)

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 becomes hFull, etc...

TypeScript type 1 (typescript-type-1) (recommended)

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

TypeScript type 2 (typescript-type-2)

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)

PureScript (purescript)

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 ]

ReScript (rescript)

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 of cssClass and returns a string
  • joinOpt: takes a list of option<cssClass> and returns a string
open Css

<div className={join([textBlue100, rounded, border, borderBlue300])}>
  {"Hello!"->React.string}
</div>

ReScript type (rescript-type)

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>

Elm (elm)

Additionally to the generated classes, you'll get 2 useful functions:

  • classes: takes a list of css classes and returns an Html.Attribute msg that can be used with any html element
  • join: performs a simple List 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!" ]

No generators

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.

Rust

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.

Pyaco validate: A type safe CSS / code validator Experimental

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...).

The Node.js API

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";

pyaco.generate({
  input: "...",
  lang: "purescript",
  outputDirectory: "...",
  watch: false,
  outputFilename: "...",
});

pyaco.validate(
  {
    cssInput: "...",
    inputGlob: "...",
    captureRegex: "...",
    maxOpenedFiles: 128,
    splitRegex: "...",
  },
  // The callback is required
  () => {
    console.log("All done");
  },
);