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:
StringOptions.timeoutfor this one compile,- the builder's
defaultTimeout, - 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.