tailwindcss

    A MoonBit implementation of the Tailwind CSS v4 compiler

    css
    tailwindcss
    compiler
    Download zip
    Version
    0.4.0
    License
    MIT
    Last updated
    17 days ago
    Downloads
    634

    Dependencies

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

    #Live demo

    https://marianoguerra.github.io/tailwindcss-moonbit/ — type HTML, watch the CSS those classes need get compiled in your browser, with the compile time and output size measured as it happens. There is no server: the compiler is this package built for wasm-gc, and the stylesheets are margaui's component library — 78 files, ~270 KB of CSS, compiled from scratch on every edit.

    The editor also closes the loop the API leaves open: it discovers candidates by parsing the HTML with moonbit-community/html and collecting class attributes, all inside MoonBit. Source and notes in web/; run it locally with just web-serve.

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

    Two sub-modes sit alongside the default compile. bundle resolves the entry's whole @import graph from the filesystem and writes a JSON { path: content } map:

    tailwindcss bundle -i input.css -o bundle.json

    That map is exactly the shape the imports field below takes, so bundle is how a filesystem project becomes a self-contained in-memory compile request for the JS / Wasm-gc entry points, which have no filesystem access. 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; }" }, "base": "", "from": "input.css", "polyfills": 3 }

    Every field except css is optional: candidates defaults to none, imports to empty, base to "", and polyfills to POLYFILL_ALL. base is the prefix that relative @import specifiers and @source paths resolve against, so it must match the keys used in imports.

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

    Two suites compare this compiler against the pinned tailwindcss@4.3.3 oracle. tools/diff/cases.json is the hand-written ledger, and tools/upstream/ is every candidate list upstream asserts on in packages/tailwindcss/src/utilities.test.ts — 738 groups over 6856 candidates, replayed through both compilers. 334 of those 360 tests match at this version; tools/upstream/baseline.json records the rest with the shape of each divergence.

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

    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.