Architecture
Warning
Jsass 6 is still in the early stages of development. API breaks are to be expected!
jsass 6 is a stack of small, replaceable pieces. The interface at the top is what your code sees; everything below it can be swapped without touching a single call site.
flowchart TB
app["Your application"]
api["jsass — JsassCompiler, StringOptions, Output, SPIs"]
v8c["jsass.javet.v8.compiler — JavetV8JsassCompiler"]
nodec["jsass.javet.node.compiler — JavetNodeJsassCompiler"]
graalc["jsass.graaljs.compiler — GraalJsJsassCompiler"]
embc["jsass.embedded.compiler — EmbeddedJsassCompiler"]
proc["dart-sass executable — sass --embedded, in a subprocess"]
javet["jsass.javet — Javet adapter, JavetOptions"]
graal["GraalJS adapter — virtual module file system, GraalJsOptions"]
core["jsass.js — JsCompilerCore, host callbacks, jsass-bridge.js"]
webjar["WebjarModuleResolver"]
nodemod["NodeModulesResolver"]
sass["dart-sass — the sass JavaScript library"]
app --> api
api --> v8c
api --> nodec
api --> graalc
api --> embc
embc -- "Embedded Sass Protocol (protobuf over stdin/stdout)" --> proc
v8c --> javet
nodec --> javet
graalc --> graal
javet --> core
graal --> core
core --> webjar
core --> nodemod
webjar --> sass
nodemod --> sass
jsassis the API and knows no engine.- The engine facades are thin builders. The three JavaScript ones pick an engine adapter and
hand it to the shared core in
jsass.js; the embedded one owns its own core, which talks to a dart-sass process instead (see below). - The engine adapters —
jsass.javetfor both Javet engines, the internal GraalJS adapter injsass.graaljs.compiler— do only four things: move plain data in both directions, expose the host callbacks as JavaScript functions, wait for the result promise to settle, and terminate a running compile. jsass.jsholds everything engine-neutral:JsCompilerCore(executor, timeouts, cancellation,close()), the host callbacks, theModuleResolvercontract and every JavaScript script jsass runs. dart-sass's JS API is known in exactly one place,jsass-bridge.js, which is identical on all engines.- Module resolvers feed dart-sass (and, with
autoprefixer(true), postcss and autoprefixer) into whichever engine is running.
The compiler interface
JsassCompiler is the only type your application needs to depend on. It has three methods —
compileString, compilePath, close — and four implementations behind them.
classDiagram
class JsassCompiler
<<interface>> JsassCompiler
JsassCompiler : +compileString(String source, StringOptions options) CompletableFuture~Output~
JsassCompiler : +compilePath(Path path, StringOptions options) CompletableFuture~Output~
JsassCompiler : +close() void
class Output
Output : String css
Output : String sourceMap
Output : List~URI~ loadedUrls
class JavetV8JsassCompiler
JsassCompiler <|-- JavetV8JsassCompiler
class JavetNodeJsassCompiler
JsassCompiler <|-- JavetNodeJsassCompiler
class GraalJsJsassCompiler
JsassCompiler <|-- GraalJsJsassCompiler
class EmbeddedJsassCompiler
JsassCompiler <|-- EmbeddedJsassCompiler
What happens during a compile
On the three JavaScript engines:
sequenceDiagram
participant App as Your code
participant C as JsCompilerCore
participant E as Engine adapter
participant B as jsass-bridge.js
participant S as dart-sass
App->>C: compileString(source, options)
C-->>App: a pending future
C->>E: acquire a runtime / context (fresh, pooled, or on a shared Engine)
E->>E: load dart-sass via the module resolver, run startup scripts
C->>B: source + options encoded as plain data, host callbacks
B->>S: compileString with dart-sass options
S-->>B: an importer, function or logger is needed
B->>C: host callback with plain data (canonicalize / load / callFunction / log)
C-->>B: plain data back (stylesheet, Sass value as a tagged map)
S-->>B: compile result
B-->>C: plain {css, sourceMap, loadedUrls}
C-->>App: complete the future
The compile runs on the compiler's executor, guarded by a timeout. Only plain data crosses the
Java/JavaScript boundary — null, booleans, numbers, strings, lists and string-keyed maps.
Behaviour crosses it as a small set of host callbacks: when dart-sass needs an importer, a custom
function or the logger, jsass-bridge.js calls back into Java with plain data, and Sass values
travel as tagged maps that the bridge turns into dart-sass objects and back. Because of that, the
engine adapters know nothing about Sass, and the three JavaScript engines behave identically.
When a callback fails — an importer or function throws — the typed exception completes the future first, then the error is thrown back into JavaScript so dart-sass unwinds. Logger failures are logged and ignored.
Backends
JavetV8JsassCompiler
The JavetV8JsassCompiler backend uses Javet in V8 mode to run dart-sass. Modules are
ECMAScript modules resolved through the ModuleResolver, typically from the classpath, so
everything ships inside JARs. A URL polyfill is installed into each runtime because bare V8 has
no WHATWG URL implementation.
JavetNodeJsassCompiler
The JavetNodeJsassCompiler backend uses Javet in Node mode. Modules typically come from a
node_modules/ directory. jsass sets each module's script name, __dirname, __filename and
require() root itself, so the process working directory stays put unless you opt in to Javet's
native behaviour — see engines.
GraalJsJsassCompiler
The GraalJsJsassCompiler backend uses GraalJS through the GraalVM Polyglot API — pure Java, no
native library. Every compile runs in a fresh, sandboxed Context on one Polyglot Engine;
modules are served through a read-only virtual file system backed by the ModuleResolver, so the
real file system and the working directory are never touched. It is fast on a matching GraalVM
JDK and interpreted elsewhere — see engines.
EmbeddedJsassCompiler
The EmbeddedJsassCompiler backend runs no JavaScript. It starts the native dart-sass executable
as sass --embedded and speaks the
Embedded Sass Protocol to it:
length-delimited protobuf messages over the process's stdin and stdout. That gives it the smallest
in-JVM footprint, at the price of a native executable that has to be present on the machine.
sequenceDiagram
participant App as Your code
participant C as Embedded core
participant P as dart-sass process
App->>C: compileString(source, options)
C-->>App: a pending future
C->>P: CompileRequest (start the process first if there is none)
P-->>C: CanonicalizeRequest / ImportRequest / FunctionCallRequest
C->>C: run your importer or function on the executor
C-->>P: the matching response
P-->>C: LogEvent (@warn, @debug) → your SassLogger
P-->>C: CompileResponse
C-->>App: complete the future
One process serves every compile of a compiler; each compile has its own id on the shared stream.
The callbacks of one compile run in order, so log events reach the logger before the future
completes. Which protocol version the executable speaks is probed once
(sass --embedded --version) and selects a protocol driver: the protocol-neutral part — process
supervision, compile bookkeeping, timeouts — does not change when a new protocol major arrives, a
new driver is added next to the existing one. Details are on the
engines page.
Design decisions worth knowing
- jsass never bundles dart-sass. The Sass version is a dependency of your build, so upgrading Sass does not wait for a jsass release.
- Files are resolved in Java, not by dart-sass, on every engine: load paths and the relative
loads of a
file:entry. dart-sass's own file lookup needs an API that only exists in the Node runtime, and its native load paths do not confine... One implementation of Sass's file rules therefore behaves identically on V8, Node, GraalJS and the embedded engine. - Every compile is bounded. A runaway stylesheet cannot pin a thread forever; the effective timeout defaults to 30 seconds.
- Error messages are sanitized by default.
getMessage()never leaks absolute filesystem paths or engine internals — the structured getters carry the full diagnostics. - Nothing is closed that jsass does not own. An injected executor, a caller-supplied Javet
engine pool, a caller-supplied Polyglot
Engineand Javet'sV8Hostsingleton all surviveclose(). The dart-sass process the embedded engine started does not. - One bridge, three JavaScript engines. Everything that knows dart-sass's JavaScript API lives
in
jsass-bridge.js. Everything that knows the Embedded Sass Protocol lives in the embedded engine's protocol driver. - One behaviour suite, four engines. The engine-neutral tests — compile, functions, importers, logger, lifecycle — run against every engine; only tests for JavaScript-only options are limited to the three JavaScript engines.