tailwindcss

A MoonBit implementation of the Tailwind CSS v4 compiler

css
tailwindcss
compiler
moon add marianoguerra/tailwindcss@0.2.0
Download zip
Version
0.2.0
License
MIT
Last updated
21 days ago
Downloads
260

Dependencies

README

#Tailwind CSS compiler for MoonBit

This package implements the compiler-facing API of Tailwind CSS v4.3.3 in MoonBit. Compilation is staged so a parsed stylesheet can be reused while new candidate classes are discovered.

///|
async test {
let compiler = @tailwindcss.compile(
"@theme { --color-black: #000; } @tailwind utilities;",
)
let css = compiler.build(["flex", "hover:bg-black"])
assert_true(css.contains("display: flex"))
assert_true(css.contains("background-color: var(--color-black)"))
}

The implementation is independent MoonBit code. tools/oracle/ contains a development-only differential runner pinned to the original npm package.

#Using it

The compiler is exposed three ways. In every case candidate discovery is the caller's responsibility: pass the class names you want generated. This package does not scan content files (HTML/JS/templates) for classes; @source directives are parsed and surfaced via Compiler::sources() for hosts that want to scan themselves.

#As a MoonBit library

compile(css, options?) resolves @imports through an async StylesheetLoader; compile_sync(css, options?) does the same through a SyncStylesheetLoader. Both return a reusable Compiler; call build with candidate class names (candidates accumulate across calls). Use compile_sync on hosts without an async runtime — notably the wasm-gc backend.

///|
test {
let loader = @tailwindcss.MemoryStylesheetLoader::new(files=[
("base.css", "@theme { --color-black: #000; }"),
])
let compiler = @tailwindcss.compile_sync(
"@import \"base.css\"; @tailwind utilities;",
options=@tailwindcss.CompileOptions::new(sync_loader=loader),
)
let css = compiler.build(["flex"])
assert_true(css.contains("display: flex"))
}

#As a native CLI

cmd/tailwindcss is a native executable:

moon build --target native cmd/tailwindcss tailwindcss -i input.css -o output.css -c candidates.txt --polyfills 3

-i/--input is the entry CSS (its @imports resolve against the filesystem via loader/fs), -c/--candidates is a newline-separated class-name file (optional), -o/--output defaults to stdout, and --polyfills is 0..3 (default all). Run tailwindcss --help for the full listing. A --batch sub-mode backs the differential test harness.

#As a JS / Wasm-gc library

The ffi package exports compile_css_json (structured, in-memory) and compile_css (inline CSS only) for the js, wasm, and wasm-gc backends. @import resolution is in-memory only: pass the imported files as a { path: content } map. compile_css_json takes a JSON request and returns a JSON { ok, css } / { ok, error } result:

{ "css": "@import \"base.css\"; @tailwind utilities;", "candidates": ["flex", "hover:bg-black"], "imports": { "base.css": "@theme { --color-black: #000; }" }, "from": "input.css", "polyfills": 3 }

ffi/js/ ships an ergonomic, dependency-free wrapper (compile({ input,candidates, imports, ... })); see ffi/js/README.md for the build and usage steps.

#Current compiler surface

The package currently covers:

  • structured CSS parsing for nested rules, at-rules, comments, strings, and balanced values
  • a Tailwind-style generic value AST for lossless nested function transformation
  • a Tailwind-oriented selector AST for structural nesting replacement, combinators, attributes, selector lists, and selector pseudo-functions
  • @theme custom properties and used-variable emission
  • @tailwind utilities, block and functional @utility, selector and block @custom-variant, stylesheet @variant, and @apply compiled through the shared candidate and variant pipeline (including variants and nested output)
  • incremental candidate accumulation
  • structured candidates with negative forms, modifiers, important flags, arbitrary properties, and stacked variants
  • display, position, overflow, flexbox, alignment, spacing, sizing, color, typography, border, opacity, and arbitrary-property utilities
  • screen-reader accessibility, table, float/clear, isolation, box-sizing, field-sizing, and basic flex-basis utilities
  • logical spacing, inset positioning, viewport/content sizing, and paired size-* utilities
  • text overflow, whitespace, wrapping, hyphenation, list, scrollbar-gutter, and vertical-alignment utilities
  • aspect-ratio, object-position, and expanded content/item/self alignment utilities
  • axis overflow, overscroll, scroll behavior, scrollbar width, user selection, resize, and basic scroll-snap utilities
  • cursor, appearance, basic touch-action, accent/caret color, backface, and transform-box utilities
  • numeric, arbitrary, and theme-backed grid templates plus auto-track and grid-flow utilities
  • numeric, negative, arbitrary, full-span, auto, and theme-backed grid item placement utilities
  • common state, structural, media, dark-mode, breakpoint, and arbitrary variants
  • @source metadata discovery
  • recursive CSS imports through an async loader, including an in-memory VFS and a native filesystem implementation
  • import layer(), supports(), and media conditions
  • --spacing(), --theme(), legacy theme(), and --alpha() substitution
  • file, inline, and negated @source directives
  • static and functional @utility definitions; functional values support theme namespaces, literals, constrained numbers/percentages, ratios, typed arbitrary values, defaults, and modifiers
  • selector shorthand and selector/at-rule block @custom-variant definitions using @slot, including multiple branches, nesting, wrapper declarations, and composition through @variant
  • theme-backed, arbitrary, named, minimum, and maximum container-query variants
  • common group-*, peer-*, and has-* compound variants, including named group/peer markers and arbitrary :has() selectors
  • structural nth-* variants plus aria-* and data-* attribute variants
  • selector-compatible not-* and in-* compound variants, including arbitrary ancestor selectors
  • functional supports-* conditions and compound negation of single media, supports, and container conditions
  • theme-backed and arbitrary min-* / max-* responsive variants
  • extended form-state, direction, starting-style, contrast, forced-color, pointer, and scripting variants
  • stateful utilities that compose through generated custom properties and @property registrations: transforms, filters, backdrop filters, shadows, rings, gradients, transitions, and touch/scroll-snap state
  • selector-producing utilities (space-x/y, divide-*, placeholder-*) with their generated child selectors and reverse custom properties
  • @reference and @import "…" reference, theme(reference) imports, import layer()/supports()/media conditions, and per-file @source base propagation
  • AST optimization: nesting flattening, adjacent rule and at-rule merging, declaration deduplication, and empty-node removal
  • the @property and color-mix() polyfills, individually gated through the polyfills compile option (POLYFILL_NONE/AT_PROPERTY/COLOR_MIX/ALL)
  • exact upstream property and variant sorting, independent of the order in which candidates are discovered

Every case in tools/diff/cases.json matches the reference compiler byte for byte apart from the upstream license banner, which this package does not print.

JavaScript configuration and plugin loading is intentionally out of scope; @config and @plugin are rejected with UnsupportedJsCompatibility. Source maps are deferred because the agreed public surface returns CSS only (see migration-plan.md step 16). Brace expansion in @source globs and the parts of the upstream utility catalogue without a passing differential case are not implemented. No input should be treated as compatible until a differential case covers it.

#Conformance

Run both the MoonBit tests and the pinned JavaScript oracle:

moon test npm install --prefix tools/oracle node tools/diff/compare.mjs

See UPSTREAM.md for provenance and the porting boundary.

#
StylesheetLoader

pub(open) trait StylesheetLoader {
async fn load(Self, id : String, base : String) -> LoadedStylesheet raise CompileError
}

Asynchronous host interface used to resolve CSS @import directives.

Use this with compile when resolution needs real I/O (for example the native filesystem loader). For hosts without an async runtime — notably the wasm-gc backend — implement SyncStylesheetLoader and call compile_sync.

#
SyncStylesheetLoader

pub(open) trait SyncStylesheetLoader {
fn load(Self, id : String, base : String) -> LoadedStylesheet raise CompileError
}

Synchronous host interface used to resolve CSS @import directives.

This is the counterpart of StylesheetLoader for compile_sync. It suits in-memory resolution (see MemoryStylesheetLoader, which implements both traits) and any host that cannot drive an async runtime, such as wasm-gc.

#
CompileError

pub(all) suberror CompileError {
InvalidCss(String)
MissingStylesheetLoader(String)
StylesheetNotFound(String)
ImportCycle(Array[String])
UnsupportedJsCompatibility(String)
InvalidApplyCandidate(String)
} derive(Eq,
Debug
)

An error reported while parsing a Tailwind stylesheet.

#
CompileOptions

pub struct CompileOptions {
base : String
from : String?
loader : &StylesheetLoader?
sync_loader : &SyncStylesheetLoader?
polyfills : Int
}

Options controlling stylesheet compilation.

#
CompileOptions::new

fn CompileOptions::new(base? : String, from? : String, loader? : &StylesheetLoader, sync_loader? : &SyncStylesheetLoader, polyfills? : Int) -> CompileOptions

Construct compile options while preserving the defaults of compile(css).

loader resolves @imports for the async compile; sync_loader does the same for compile_sync. Supply whichever matches the entry point in use.

#
Compiler

pub struct Compiler {
input : String
polyfills : Int
stylesheet : Array[CssNode]
theme : Map[String, String]
sources : Array[Source]
candidates : Map[String, Unit]
excluded_candidates : Map[String, Unit]
custom_utilities : Map[String, Array[CssNode]]
functional_utilities : Map[String, Array[CssNode]]
custom_variants : Map[String, CustomVariantTemplate]
has_utilities : Bool
}

The reusable result of parsing a Tailwind stylesheet.

Pass candidate class names to build. Candidates accumulate, matching the incremental behavior of Tailwind CSS v4's JavaScript compiler.

#
Compiler::build

fn Compiler::build(self : Compiler, candidates : ArrayView[String]) -> String

Build CSS for all candidates seen so far.

Unknown candidates are ignored, as in the reference compiler.

#
Compiler::sources

fn Compiler::sources(self : Compiler) -> Array[Source]

Return sources discovered while compiling the stylesheet.

#
CssNode

#
CustomVariantTemplate

type CustomVariantTemplate

#
LoadedStylesheet

pub(all) struct LoadedStylesheet {
content : String
path : String
base : String
} derive(Eq,
Debug
)

A stylesheet returned by a StylesheetLoader.

#
MemoryStylesheetLoader

pub struct MemoryStylesheetLoader {
files : Map[String, String]
}

An asynchronous in-memory stylesheet loader.

Paths are normalized on insertion and lookup. This makes it suitable for deterministic tests and hosts without filesystem access.

#
MemoryStylesheetLoader::add

fn MemoryStylesheetLoader::add(self : MemoryStylesheetLoader, path : String, content : String) -> Unit

#
MemoryStylesheetLoader::new

fn MemoryStylesheetLoader::new(files? : Array[(String, String)]) -> MemoryStylesheetLoader

#
Source

pub(all) struct Source {
base : String
pattern : String
negated : Bool
} derive(Eq, ToJson,
Debug
)

A source pattern discovered in an @source directive.

#
SourceSpan

type SourceSpan derive(Eq,
Debug
)

#
POLYFILL_ALL

let POLYFILL_ALL : Int

Emit every optional polyfill, which is the default.

#
POLYFILL_AT_PROPERTY

let POLYFILL_AT_PROPERTY : Int

Emit the @layer properties fallback for registered custom properties.

#
POLYFILL_COLOR_MIX

let POLYFILL_COLOR_MIX : Int

Emit the color-mix() fallback guarded by @supports.

#
POLYFILL_NONE

let POLYFILL_NONE : Int

Emit no optional polyfill.

#
collect_imports

async fn collect_imports(css : String, loader : &StylesheetLoader, base? : String) -> Map[String, String] raise CompileError

Walk the @import/@reference graph of css and collect every transitively imported stylesheet as a path -> content map.

The keys are the exact lexical paths MemoryStylesheetLoader uses for lookup (resolve_path(base, id)), with the key chain seeded at "". The map is therefore directly usable as the imports field of a compile request (e.g. compile_css_json / the JS compile({ input, imports }) wrapper) whenever the entry css is compiled with the default base = "" — resolution replays the same paths and finds every file.

Content is read through loader. base~ is only the entry's on-disk resolution base (the directory of the entry file) used for filesystem reads; it does not affect the emitted keys. This lets a real filesystem loader resolve against actual directories no matter where the entry lives, while the keys stay portable (entry-relative, no absolute prefixes).

#
compile

async fn compile(css : String, options? : CompileOptions) -> Compiler

Parse a Tailwind CSS v4 stylesheet and return a reusable compiler.

Resolves @imports through the async options.loader. Hosts without an async runtime (e.g. wasm-gc) should use compile_sync instead.

#
compile_sync

fn compile_sync(css : String, options? : CompileOptions) -> Compiler raise CompileError

Synchronous variant of compile, resolving @imports through options.sync_loader.

This is the entry point for hosts that cannot drive an async runtime, most notably the wasm-gc backend. Behaviour is otherwise identical to compile; MemoryStylesheetLoader implements the required SyncStylesheetLoader.