Project

sasso

0.0
The project is in a healthy, maintained state
Embeddable, dependency-free SCSS/Sass compiler aiming at byte-for-byte dart-sass parity, shipped as a Rust native extension (magnus + rb-sys).
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0

Runtime

~> 0.9.111
 Project Readme

sasso (Ruby)

Gem Version

In-process SCSS / Sass → CSS compilation for Ruby, backed by sasso — a pure-Rust, dependency-free dart-sass alternative that targets byte-for-byte parity with current dart-sass. Shipped as a native extension (Rust via magnus + rb-sys); no Node, no subprocess, no Dart VM.

This gem is the Ruby binding. The compiler core lives in the separate momiji-rs/sasso repo (crate on crates.io); this gem pins it exactly and carries its version number.

Install

# Gemfile
gem "sasso"
$ bundle install

Ruby 3.2, 3.3, 3.4 and 4.0 are supported, and CI tests each of them on Linux and macOS. Ruby 4.0 needs sasso 0.14.1 or later.

Precompiled native gems for all four Ruby versions ship for these platforms, so they need no Rust toolchain:

OS platforms
Linux (glibc) x86_64-linux, aarch64-linux
Linux (musl) x86_64-linux-musl, aarch64-linux-musl
macOS x86_64-darwin, arm64-darwin
Windows x64-mingw-ucrt

On any other platform the gem compiles from source with cargo, which needs a Rust toolchain.

Usage

require "sasso"

Sasso.compile_string("$c: #336699; a { color: $c; &:hover { color: red } }")
# => "a {\n  color: #336699;\n}\na:hover {\n  color: red;\n}\n"

# Minified:
Sasso.compile_string("a { b: 1px }", style: :compressed)   # => "a{b:1px}"

# Indented .sass syntax:
Sasso.compile_string("a\n  b: 1px\n", indented: true)

# A file (syntax inferred from the extension; diagnostics get the file name):
Sasso.compile("app/assets/stylesheets/application.scss",
              style: :compressed, load_paths: ["vendor/stylesheets"])

Options (compile_string / compile)

Option Default Meaning
style: :expanded :expanded or :compressed
syntax: :scss :scss, :sass, or :css
indented: false shorthand for syntax: :sass
load_paths: [] directories searched for @use/@forward/@import
url: nil filename shown in diagnostics (enables the rich dart-style error block)
alert_ascii: false ASCII-only diagnostics
charset: true prefix non-ASCII output with @charset "UTF-8"; (a BOM when compressed)
source_map: false return a Sasso::CompileResult instead of a String
source_map_include_sources: false embed each source's text in the map's sourcesContent
quiet: false print no @warn/@debug/deprecation diagnostics
quiet_deps: false drop deprecation warnings raised inside dependencies
silence_deprecations: [] deprecation ids (Strings or Symbols) whose warnings are dropped
on_warn: nil a callable receiving each diagnostic; replaces the stderr printing

Source maps

With source_map: true the return value is a Sasso::CompileResult — #css is the same String you would get otherwise, and #source_map is a parsed Source Map v3 Hash. Pass url: so the map can name the entry stylesheet.

r = Sasso.compile_string(scss, source_map: true, url: "application.scss")
r.css                     # => "a {\n  color: red;\n}"
r.source_map["version"]   # => 3
r.source_map["sources"]   # => ["application.scss"]

Diagnostics

@warn, @debug and deprecation warnings print to $stderr by default, the same dart-style block the sasso CLI and dart-sass print. To change that:

# Silence them entirely:
Sasso.compile_string(scss, quiet: true)

# Silence only what dependencies deprecate (dart-sass `quietDeps`) — files
# resolved through a load path. The entry stylesheet's own still print.
Sasso.compile_string(scss, load_paths: ["vendor/stylesheets"], quiet_deps: true)

# Silence particular deprecations by id (dart-sass `silenceDeprecations`),
# wherever they are raised. Every other diagnostic still prints.
Sasso.compile_string(scss, silence_deprecations: [:import, "color-functions"])

# Or take delivery yourself, which suppresses the printing:
Sasso.compile_string(scss, url: "in.scss", on_warn: ->(d) {
  Rails.logger.warn(d[:formatted])   # the block the compiler would have printed
})

Each diagnostic is a Hash of Sasso::WARNING_KEYS: :kind (:warn/:debug), :deprecation, :deprecation_id, :message, :formatted, :url, :line and :path. :url is dart's display form of the file; :path identifies it (the importer's canonical path), which is what distinguishes a dependency from the entry stylesheet. quiet: and on_warn: are mutually exclusive.

Two things to know about what on_warn: receives:

  • A compile that warns and then fails delivers its warnings first and raises Sasso::CompileError after, so a diagnostic is never lost to the error — the callable is the only thing reporting them once it has taken over. If the callable itself raises, the CompileError still wins and carries that exception as its #cause.
  • Like dart-sass, the compiler caps a repeated deprecation at five per id and then emits one summary diagnostic — "N repetitive deprecation warnings omitted." — with no :deprecation_id, no :url and :line of 0. Code that keys on the id or formats "#{d[:url]}:#{d[:line]}" should expect it. There is no verbose mode that lifts the cap; the compiler core does not offer one.

Versions

Since 0.14.0 the gem version tracks the core compiler crate it bundles: gem 0.14.0 pins crate 0.14.0. A gem-only fix takes the next patch, so Sasso::VERSION may sit ahead of the crate within a minor — Sasso::CORE_VERSION reads the version out of the linked binary and is the authority on what is loaded.

Sasso::VERSION       # => "0.14.0"  the gem
Sasso::CORE_VERSION  # => "0.14.0"  the compiler actually linked in

Errors

A compile failure raises Sasso::CompileError (a Sasso::Error < StandardError) whose #message is the compiler's full diagnostic — the same text the sasso CLI prints.

begin
  Sasso.compile_string("a { b: 1px + 1em }")
rescue Sasso::CompileError => e
  warn e.message
end

Framework integrations

Using a Ruby web framework? These drop-in gems build on this one and compile your Sass/SCSS in-process (no Node, no Dart, no subprocess), byte-for-byte identical to dart-sass — typically ~6–7× faster per compile than the Node sass default:

Conformance

The core passes 100% of the attempted official sass-spec suite byte-for-byte against dart-sass; see the core repo. For the inputs below, sasso's output is byte-identical to sass-embedded (dart-sass).

Performance

Because sasso compiles in-process (a direct Rust call — no subprocess, no IPC, no Dart VM), it avoids the per-call protocol roundtrip of sass-embedded and the process-spawn cost paid by any out-of-process compiler.

Compiling the same SCSS (variables, nesting, @for, math) on an Apple M2 Max, Ruby 3.4.1, against sass-embedded 1.104.1 and sassc 2.4.0:

sasso (this gem) sass-embedded (dart-sass) sassc (libsass)
Warm — small component (256 B) 13.3 µs 125 µs (9.4×) 1148 µs (87×)
Warm — ~180 rules (5.5 KB) 215 µs 906 µs (4.2×) 10234 µs (48×)
Cold start (require + first compile) 3.2 ms 40.6 ms (12.7×) 37.0 ms (12×)

Parenthesised values are how much slower the other gem is than sasso.

  • Per-request compiling (e.g. a Sinatra route): in-process latency is ~13 µs vs ~125 µs for sass-embedded's pipe roundtrip to its Dart subprocess.
  • One-shot builds (e.g. rails assets:precompile): the dominant cost is the ~41 ms Dart subprocess spawn, which sasso does not pay (~3 ms cold).

The engine is also heavily perf-tuned (a scoped bump arena, reference-counted values). Numbers are representative of one machine; run your own with your stylesheets.

License

MIT, matching the Sass ecosystem (the core sasso compiler crate remains dual-licensed MIT OR Apache-2.0).