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: thesassWebJar or anode_modules/sassdirectory; - the native dart-sass executable (1.79.1 or newer) from a dart-sass release, for the embedded engine.
- a JavaScript engine: the Javet native
library for your platform (V8 or Node), or GraalJS (
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.
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
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")
}
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")
}
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.
<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
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());
}
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.
@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.javaconsumer has to add. - Error handling — what a failed future carries.
- Examples — runnable projects in the repository.