Skip to content

0.7.0 - 2026-07-04

Latest

Choose a tag to compare

@github-actions github-actions released this 04 Jul 21:36

Release Notes

Crate release v0.7.0; ships on npm as sasso@0.10.0 (wasm + native — same
core). The dart-sass byte-parity campaign: every project in the new
real-world corpus (bench/real-world/real_world.md)
now compiles, and most match dart-sass 1.101 byte-for-byte.

Added (npm package — sasso/native; released as sasso@0.9.0 on npm)

  • sasso/native subpath: the native addon as a first-class npm entry.
    Prebuilt binaries publish as exact-version-pinned optionalDependencies
    (sasso-native-{darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu}) —
    npm installs only the matching platform, the release workflow builds and
    byte-parity-tests every binary against the wasm reference before publishing,
    and unsupported platforms get a clear error pointing back at the wasm
    entries. Resolution order: SASSO_NATIVE_BINARY override → platform
    package → repo-local build.

Added (repo — native Node addon)

  • napi/: a native Node addon binding the core crate directly (F4 of
    docs/ASYNC_PERF_ARCHITECTURE.md) — no wasm, no asyncify. Same dart-sass
    modern API as the sasso npm package, verified byte-identical to the
    wasm engine's output across the in-repo corpora (which is itself dart-sass
    byte-exact). Async compiles each run on their own OS thread: ~3× engine
    speed over the wasm modules, and a concurrent 8-entry cold build finishes
    in ~1.14× a single compile's time (true multi-core parallelism; the wasm
    engine's single JS thread serializes CPU-bound fan-out). User importers, custom functions,
    and loggers bridge to JS; loadPaths/relative resolution run natively.
    Repo-buildable (bash napi/build.sh, node napi/test.mjs); publishing
    waits on a per-platform prebuild matrix.

Changed (npm package — async path performance; released as sasso@0.8.0 on npm)

  • Concurrent compileStringAsync/compileAsync calls no longer serialize.
    The single asyncify-instance lock is replaced by a lazily-grown pool of
    asyncify engines (default cap: min(4, cpu cores), tunable via the new
    configure({ asyncInstances })). While one compile awaits an asynchronous
    importer, other compiles run on other engines — a bundler fanning out N
    sass entries no longer queues them end-to-end (measured: N=8 fan-out with
    2 ms importer latency, makespan −75.6%). A process that never overlaps
    async compiles still pays for exactly one instance; each additional engine
    reserves its own wasm memory (incl. the arena) plus a 1 MiB asyncify stack.
  • Synchronously-resolving importers and custom functions no longer pay the
    asyncify suspension on the async APIs.
    Results that settle synchronously
    (plain return values — including the built-in loadPaths filesystem chain
    and sass-loader's cache-hit resolutions) are delivered without an
    unwind/rewind cycle. A loadPaths-only compileStringAsync now suspends
    zero times. Genuinely-async importers behave exactly as before.
  • sasso/speed's async APIs now run a speed-optimized (-O3) asyncify
    module
    (sasso.speed.async.wasm, ~3.2 MB / 1.0 MB gzip) instead of
    sharing the size-optimized one — ~2× engine throughput at v8 steady state
    (long-lived processes; one-shot CLI-style runs are dominated by v8 tiering
    and see little change). The default sasso entry is unchanged.
  • Degraded async modules (built without wasm-opt) previously crashed on the
    first importer callback; they now work for synchronously-resolving chains
    and reject genuinely-async importers with a clear error.

Fixed

  • Real-world corpora now compile — four previously failed. Callable
    closures capture the defining file's @use namespace tables (dart
    Environment.closure()), fixing "There is no module with the namespace
    X" for functions/mixins reached via @import or multi-hop @forward
    (uswds's units(), quasar's str-fe()). CSS escapes are literal
    identifier text in selector scans (.govuk-\!-font-size-19,
    govuk-frontend). In the indented syntax, as * terminates a
    @use/@forward prelude instead of reading as a pending multiplication
    (vuetify), and a trailing-comma selector line with a pseudo-glued colon
    (&:active, / i[type="s"]::-webkit-x,) continues the list instead of
    being silently dropped (quasar — a correctness bug, not formatting).
  • Byte-parity with dart-sass across the serialization surface. Loud
    comments dedent at serialize time (interpolated banners included) and an
    indented /** opener stays glued; comments registered before a module's
    first load re-emit at every dependency edge (bulma's /* Bulma Form */);
    @imported files carry their own file identity (no cross-file trailing
    -comment gluing); invisible @extend-only rules leave no blank-line group
    end; selector lists keep their authored line structure inside nested
    at-rule wraps and plain-CSS imports (which also unquote identifier
    attribute values like dart's parser); multi-& parent expansion
    interleaves column-major (mastodon's adjacent-state selectors); a &
    nested in pseudo parens substitutes inside multi-& parts
    (:not(&--mini-animate)).
  • Chained @extend products keep dart's registration order. Each
    @extend's extender list is pre-extended by the store accumulated so far
    (dart's addSelector), so .navbar > .container, … .container-xxl comes
    out in forward order instead of reversed.
  • Errors inside loaded files are attributed to that file. The snippet
    renders from the erring file and the trace stacks one frame per loader
    (_mod.scss 1:13 @use / main.scss 1:1 root stylesheet), matching
    dart for @use, @forward, and @import chains — parse errors
    included. Previously the root file's name and snippet were shown with the
    inner file's line numbers.
  • @media nested inside an unknown at-rule now compiles. dart's
    _inUnknownAtRule context legalizes bare declarations without an enclosing
    style rule, so the canonical Tailwind v4 idiom
    @utility container { @media (width >= 96rem) { max-width: 87.5rem; } }
    parses and emits verbatim (byte-matched to dart-sass 1.101, including the
    classic min-width syntax, interpolated queries, and mixed
    declaration-plus-@media bodies). A bare declaration in a top-level
    @media still errors like dart. Previously: Error: expected "{".
  • Keyframe selector lists now join on one line, matching dart-sass. A
    multi-line authored frame selector (0%,\n60%,\n100% {) was emitted with
    the author's line breaks preserved, as style-rule selector lists are; dart
    re-serializes keyframe stops joined with ", " and drops the breaks
    (0%, 60%, 100% {). Found compiling a real-world Rails corpus (a Bootstrap
    → Tailwind compat layer) where this was the only byte difference across
    ~132 KB of output.

Install sasso 0.7.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/momiji-rs/sasso/releases/download/v0.7.0/sasso-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/momiji-rs/sasso/releases/download/v0.7.0/sasso-installer.ps1 | iex"

Download sasso 0.7.0

File Platform Checksum
sasso-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
sasso-x86_64-apple-darwin.tar.xz Intel macOS checksum
sasso-x86_64-pc-windows-msvc.zip x64 Windows checksum
sasso-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
sasso-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum
sasso-aarch64-unknown-linux-musl.tar.xz ARM64 MUSL Linux checksum
sasso-x86_64-unknown-linux-musl.tar.xz x64 MUSL Linux checksum