Skip to content

Getting started

Warning

jsass 6 is still in the early stages of development. API breaks are to be expected!

Requirements

  • Java 21 or newer. jsass 6 is compiled to Java 21 bytecode and is tested on 21 and 25.
  • Something to run dart-sass, one of:
    • a JavaScript engine: the Javet native library for your platform (V8 or Node), or GraalJS (org.graalvm.polyglot:js-community, pure Java, Linux and macOS only), plus a copy of dart-sass's JavaScript build: the sass WebJar or a node_modules/sass directory;
    • the native dart-sass executable (1.79.1 or newer) from a dart-sass release, for the embedded engine.

jsass never downloads dart-sass for you. See compatibility.

1. Add the repository

6.0.0-alpha.4 is not on Maven Central. Pre-releases live in this project's GitLab package registry, which is world-readable — no token, no login.

build.gradle.kts
repositories {
    mavenCentral()
    maven {
        name = "jsass"
        url = uri("https://gitlab.com/api/v4/projects/11056061/packages/maven")
    }
}
pom.xml
<repositories>
  <repository>
    <id>jsass</id>
    <url>https://gitlab.com/api/v4/projects/11056061/packages/maven</url>
  </repository>
</repositories>

Stable 5.x releases remain on Maven Central; 6.x will be published there again once the API settles.

2. Pick your pieces

A setup on a JavaScript engine answers three questions; the embedded engine needs only the first two. The defaults below are the V8 route, which needs no Node.js installation and no node_modules directory.

Decision V8 route Node route GraalJS route Embedded route
Which engine? jsass.javet.v8.compiler jsass.javet.node.compiler jsass.graaljs.compiler jsass.embedded.compiler
What runs it? com.caoccao.javet:javet-v8-<platform> com.caoccao.javet:javet-node-<platform> org.graalvm.polyglot:js-community the dart-sass executable
Where does Sass live? jsass.webjar-module-resolver jsass.node-modules-resolver either of the two in the executable
Who parses package.json? jsass.jackson2 or jsass.jackson3 jsass.jackson2 or jsass.jackson3 jsass.jackson2 or jsass.jackson3 nobody

The engines page compares the four engines; the JSON SPI page explains why a JSON parser is part of the picture at all. The module resolvers work on every JavaScript engine — the pairings above are the usual ones, not the only ones.

3. Add the dependencies

build.gradle.kts
dependencies {
    // brings io.bit3:jsass, io.bit3:jsass.js, io.bit3:jsass.javet and com.caoccao.javet:javet
    implementation("io.bit3:jsass.javet.v8.compiler:6.0.0-alpha.4")
    implementation("io.bit3:jsass.webjar-module-resolver:6.0.0-alpha.4")
    // discovered through ServiceLoader — exactly one of jackson2 / jackson3
    runtimeOnly("io.bit3:jsass.jackson2:6.0.0-alpha.4")

    // dart-sass itself, and the native engine for your platform
    runtimeOnly("org.webjars.npm:sass:1.86.3")
    runtimeOnly("com.caoccao.javet:javet-v8-linux-x86_64:6.0.2")

    // any SLF4J provider
    runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
}
build.gradle.kts
dependencies {
    // brings io.bit3:jsass, io.bit3:jsass.js and org.graalvm.polyglot:polyglot
    implementation("io.bit3:jsass.graaljs.compiler:6.0.0-alpha.4")
    implementation("io.bit3:jsass.webjar-module-resolver:6.0.0-alpha.4")
    runtimeOnly("io.bit3:jsass.jackson2:6.0.0-alpha.4")

    // dart-sass itself, and the JavaScript language for GraalVM (or org.graalvm.polyglot:js)
    runtimeOnly("org.webjars.npm:sass:1.86.3")
    runtimeOnly("org.graalvm.polyglot:js-community:25.4.4.1.1")

    runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
}
build.gradle.kts
dependencies {
    // brings io.bit3:jsass and com.google.protobuf:protobuf-java
    implementation("io.bit3:jsass.embedded.compiler:6.0.0-alpha.4")

    runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
}

No dart-sass dependency: the executable is installed on the machine, see below.

pom.xml
<dependencies>
  <dependency>
    <groupId>io.bit3</groupId>
    <artifactId>jsass.javet.v8.compiler</artifactId>
    <version>6.0.0-alpha.4</version>
  </dependency>
  <dependency>
    <groupId>io.bit3</groupId>
    <artifactId>jsass.webjar-module-resolver</artifactId>
    <version>6.0.0-alpha.4</version>
  </dependency>
  <dependency>
    <groupId>io.bit3</groupId>
    <artifactId>jsass.jackson2</artifactId>
    <version>6.0.0-alpha.4</version>
    <scope>runtime</scope>
  </dependency>

  <!-- dart-sass itself, and the native engine for your platform -->
  <dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>sass</artifactId>
    <version>1.86.3</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.caoccao.javet</groupId>
    <artifactId>javet-v8-linux-x86_64</artifactId>
    <version>6.0.2</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

jsass.javet.v8.compiler pulls in jsass, jsass.js, jsass.javet and com.caoccao.javet:javet transitively, and the WebJar resolver adds org.webjars:webjars-locator-core. The native engine artifact is the one you must name yourself — it is platform-specific, so no POM can choose it for you. It must have the same version as the javet jar jsass brings (6.0.2); a native library from another Javet release does not fit.

The embedded route has no Maven dependency for dart-sass at all: install a dart-sass release (1.79.1 or newer) and point jsass at its sass launcher, or put it on the PATH — see finding the executable.

The GraalJS route has no native artifact at all. Its js-community (or js) dependency is the JavaScript language for the Polyglot API jsass compiles against — pick the version matching your JDK if you run on a GraalVM JDK, see the GraalJS engine.

Cross-platform builds

Declare every Javet native you ship for. The artifacts do not conflict; Javet loads the one that matches the running OS and architecture. See compatibility for the list.

4. Compile something

Compile an inline SCSS string
import io.bit3.jsass.StringOptions;
import io.bit3.jsass.javet.v8.JavetV8JsassCompiler;
import io.bit3.jsass.js.webjars.WebjarModuleResolver;
import java.net.URI;
import java.util.concurrent.TimeUnit;

var moduleResolver = WebjarModuleResolver.builder().build();

try (var compiler = JavetV8JsassCompiler.builder()
    .moduleResolver(moduleResolver)
    .build()) {

  var options = StringOptions.builder()
      .url(URI.create("virtual:root.scss"))
      .sourceMap(false)
      .build();

  var output = compiler
      .compileString("$c: #36f; .button { color: $c; }", options)
      .get(30, TimeUnit.SECONDS);

  System.out.println(output.getCss());
}
Output
.button {
  color: #36f;
}

The resolver finds its JSON deserializer on its own: jsass.jackson2 on the runtime classpath is discovered through ServiceLoader (see JSON SPI).

On GraalJS only the builder changes:

import io.bit3.jsass.graaljs.GraalJsJsassCompiler;

try (var compiler = GraalJsJsassCompiler.builder()
    .moduleResolver(WebjarModuleResolver.builder().build())
    .build()) {
  // compileString(...) exactly as above
}

On the embedded engine there is no module resolver; the builder takes the executable instead:

import io.bit3.jsass.embedded.EmbeddedJsassCompiler;

try (var compiler = EmbeddedJsassCompiler.builder()
    .sassExecutable(Path.of("/opt/dart-sass/sass")) // or -Djsass.embedded.sass-executable, or PATH
    .build()) {
  // compileString(...) exactly as above
}

Compiling a file from disk is the same call with a Path:

var output = compiler.compilePath(Path.of("src/main/scss/app.scss"), options)
    .get(30, TimeUnit.SECONDS);

The compiler is a singleton, not a helper

Create one compiler and keep it for the lifetime of your application — a Spring bean, a static field, whatever fits. It owns an executor, the module resolver and its caches, and (if you configure one) the engine pool or Polyglot engine that makes repeated compiles cheap — or, on the embedded engine, the dart-sass process.

Spring Boot
@Bean
public JsassCompiler jsassCompiler() {
  var moduleResolver = WebjarModuleResolver.builder().build();

  return JavetV8JsassCompiler.builder().moduleResolver(moduleResolver).build();
}

A compiler is safe to call from multiple threads: each compile runs on the compiler's executor. close() stops accepting new compiles, waits for the in-flight ones and releases the compiler-owned executor. It is idempotent, and after it every compileString / compilePath returns a future that has already failed with IllegalStateException.

Compiling a lot?

Out of the box every Javet compile gets a fresh JavaScript runtime, which means dart-sass is evaluated again each time. An engine pool reuses runtimes — and with them the already-compiled Sass modules — and is the single biggest win for a service that compiles continuously. On GraalJS the equivalent is a shared Engine. The embedded engine keeps one dart-sass process per compiler anyway.

Next steps

  • Options — source maps, load paths, output style, deprecation handling.
  • Custom functions — call back into Java from SCSS.
  • Importers — serve stylesheets from a database, a WebJar, or thin air.
  • Engines — V8, Node, GraalJS or embedded, and everything the builders can do.
  • Java modules — what a module-info.java consumer has to add.
  • Error handling — what a failed future carries.
  • Examples — runnable projects in the repository.