Skip to content

Engines

A JsassCompiler is a thin Java façade over a JavaScript runtime that has dart-sass loaded into it. jsass ships two bindings for that runtime, both built on Javet. They implement the same JsassCompiler interface — switching is a change of builder and of two dependencies.

V8 or Node?

JavetV8JsassCompiler JavetNodeJsassCompiler
Artifact jsass.javet.v8.compiler jsass.javet.node.compiler
Where dart-sass comes from a sass WebJar on the classpath a node_modules/sass directory on disk
Module resolver WebjarV8ModuleResolver NodeModulesResolver
Needs a Node installation no no — the Node runtime is embedded, but you need a populated node_modules/
JS ecosystem available ECMAScript modules only the Node API surface (fs, require, …)
Typical use server applications, build tools, anything that ships as a JAR pipelines that already have an npm tree, PostCSS / Autoprefixer post-processing

When in doubt, take V8. It has the fewer moving parts: one WebJar dependency, no filesystem layout to get right, and a self-contained JAR at the end.

The V8 engine

var moduleResolver = WebjarV8ModuleResolver.builder()
    .jsonDeserializer(new Jackson2JsonDeserializer())
    .build();

var compiler = JavetV8JsassCompiler.builder()
    .moduleResolver(moduleResolver)
    .build();

WebjarV8ModuleResolver resolves the JavaScript import statements dart-sass makes against WebJar resources on the classpath. Its builder accepts:

Property Default Purpose
jsonDeserializer (required) reads each package's package.json to find its entry point
locator new WebJarAssetLocator() share one instance if you also use WebjarImporter
classLoader the resolver's own for container setups with an isolated classpath
charset UTF-8 encoding of the JavaScript sources

The V8 binding also installs a URL polyfill into every runtime, because dart-sass expects the WHATWG URL API that bare V8 does not provide.

The Node engine

var moduleResolver = NodeModulesResolver.builder()
    .nodeModulesBasePath(Path.of("node_modules").toAbsolutePath())
    .jsonDeserializer(new Jackson2JsonDeserializer())
    .build();

var compiler = JavetNodeJsassCompiler.builder()
    .moduleResolver(moduleResolver)
    .build();

nodeModulesBasePath is mandatory and is a hard boundary: a module name that would resolve outside it fails with SassPathTraversalException rather than falling through to somewhere else on disk.

PostCSS and Autoprefixer

The Node binding can post-process the compile result before it is handed back to Java. PostcssHelper wires the postcss and autoprefixer modules into the builder and appends a result callback that runs them over the generated CSS and its source map:

var builder = JavetNodeJsassCompiler.builder().moduleResolver(moduleResolver);
var compiler = PostcssHelper.enableAutoprefixer().apply(builder).build();

Both packages have to be present in the same node_modules/ tree.

Shared builder options

Everything below applies to both engines.

Logging

javetLogger(new JavetSlf4JLogger()) routes Javet's own diagnostics into SLF4J. Without it, engine-level messages are dropped. This is separate from the Sass logger, which reports @warn and @debug from your stylesheets.

Runtime customizers

A JavetRuntimeCustomizer runs against every JavaScript runtime right after it is created — the hook for injecting globals or polyfills:

JavetRuntimeCustomizer banner =
    runtime -> runtime.getExecutor("globalThis.__BANNER__ = 'v8';").executeVoid();

var compiler = JavetV8JsassCompiler.builder()
    .moduleResolver(moduleResolver)
    .runtimeCustomizer(banner)
    .build();

Timeouts

Every compile is bounded. The effective limit is resolved in this order:

  1. StringOptions.timeout for this one compile,
  2. the builder's defaultTimeout,
  3. 30 seconds, the built-in floor.

Exceeding it fails the future with JsassCompilationTimeoutException. Both values must be positive; a zero or negative Duration is rejected when the builder or the options object is constructed.

Executor

By default the compiler creates and owns a fixed thread pool: as many threads as the engine pool allows runtimes, or as many as there are CPUs when there is no pool, capped at 64 either way. Pass your own with executor(…) to run compiles on an application-managed pool — an injected executor is not shut down by close(), since jsass does not own it.

Engine pools

Without a pool, every compile creates a fresh JavaScript runtime and closes it again when the future completes. That is the safe default — no state survives between compiles — but it also means dart-sass is loaded and evaluated from scratch every single time, and the module resolver's per-runtime cache of compiled modules is thrown away with it.

An engine pool reuses runtimes across compiles, so the Sass modules are compiled once per runtime instead of once per compile:

var config = new JavetEngineConfig();
config.setJSRuntimeType(JSRuntimeType.V8);
config.setPoolMinSize(1);
config.setPoolMaxSize(4);

try (var pool = new JavetEnginePool<V8Runtime>(config);
     var compiler = JavetV8JsassCompiler.builder()
         .moduleResolver(moduleResolver)
         .enginePool(pool)
         .build()) {
  // …
}

The pool's JSRuntimeType has to match the binding — a Node pool handed to the V8 compiler is rejected with IllegalArgumentException at construction time. The pool is yours: close() on the compiler never closes it, so close it yourself, after the compiler.

Pooled runtimes and allowEval

Javet's engine configuration disables code generation from strings by default, which dart-sass needs while its module graph evaluates. jsass 6.0.0-alpha.1 fixed this for pooled runtimes; on older builds a pooled compile fails where an unpooled one succeeds.