Migrating from jsass 5
jsass 6 is not a drop-in upgrade. libsass and dart-sass are different compilers with different APIs, and jsass 6 follows dart-sass closely rather than preserving the libsass-shaped surface. Expect to touch every place that talks to the compiler — but only those places; your SCSS is mostly unaffected.
Both can coexist
jsass 5 stays on Maven Central under the same io.bit3:jsass coordinates. Nothing forces the
move until you are ready.
The shape of the change
var moduleResolver = WebjarV8ModuleResolver.builder()
.jsonDeserializer(new Jackson2JsonDeserializer())
.build();
try (var compiler = JavetV8JsassCompiler.builder()
.moduleResolver(moduleResolver)
.build()) {
var options = StringOptions.builder()
.url(URI.create("virtual:root.scss"))
.style(StringOptions.OutputStyle.COMPRESSED)
.build();
var output = compiler.compileString(source, options).get(30, TimeUnit.SECONDS);
System.out.println(output.getCss());
} catch (ExecutionException e) {
// e.getCause() is a JsassCompilationException for Sass errors
}
Four differences to internalise:
- You choose an engine.
new Compiler()is gone; a compiler is built from an engine binding plus a module resolver. See getting started. - Compiles are asynchronous. Every call returns a
CompletableFuture<Output>and is bounded by a timeout. - Options are immutable. A Lombok builder replaces the setter-per-option object, and one type
(
StringOptions) covers both string and file compiles. - The compiler is a resource. It is
AutoCloseableand meant to live as long as your application, not as long as one compile.
Entry points
| jsass 5 | jsass 6 |
|---|---|
compiler.compileString(source, options) |
compiler.compileString(source, options) → future |
compiler.compileFile(inputUri, outputUri, options) |
compiler.compilePath(path, options) → future |
output.getCss() |
output.getCss() |
output.getSourceMap() |
output.getSourceMap() — null unless sourceMap(true) |
| — | output.getLoadedUrls() — every file the compile touched |
jsass 6 does not write files. compileFile's output URI has no counterpart: take
output.getCss() and write it where you want it.
Options
| jsass 5 | jsass 6 |
|---|---|
setOutputStyle(NESTED \| COMPACT) |
gone — dart-sass dropped both; use EXPANDED |
setOutputStyle(EXPANDED \| COMPRESSED) |
.style(OutputStyle.EXPANDED \| COMPRESSED) |
setIsIndentedSyntaxSrc(true) |
.inputSyntax(Syntax.INDENTED) |
getIncludePaths().add(new File(…)) |
.loadPaths(List.of(Path.of(…))) |
getImporters().add(…) |
.importers(List.of(…)) — different SPI, see below |
getHeaderImporters().add(…) |
gone — prepend the header to the source, or use an importer |
getFunctionProviders().add(…) |
.functions(Map.of("signature", SassFunction…)) |
setSourceMapFile(new File(…)) |
.sourceMap(true) and write output.getSourceMap() yourself |
setSourceMapContents(true) |
.sourceMapIncludeSources(true) |
setSourceMapEmbed(true) |
gone — embed the map yourself if you need a data URI |
setOmitSourceMapUrl(true) |
gone — jsass never appends a sourceMappingURL |
setSourceComments(true) |
gone — dart-sass has no equivalent |
setPrecision(6) |
gone — dart-sass uses a fixed precision and does not expose it |
setIndent("\t") / setLinefeed("\r\n") |
gone — dart-sass does not make the output configurable |
| — | .timeout(Duration), .logger(…), .quietDeps(…), .fatalDeprecations(…), .silenceDeprecations(…), .alertAscii(…), .alertColor(…), .charset(…), .url(…) |
The full list is on the options page.
Custom functions
jsass 5 scanned a function provider object with reflection and mapped its public methods:
public class MyFunctions {
public String hello(@Name("name") String name) {
return "Hello " + name;
}
}
options.getFunctionProviders().add(new MyFunctions());
jsass 6 registers callbacks explicitly, under their SCSS signature, with typed values:
SassCallback hello = ctx -> {
var name = ((SassString) ctx.getArgs().get(0)).getValue();
return SassString.quoted("Hello " + name);
};
var options = StringOptions.builder()
.functions(Map.of("hello($name)", SassFunction.builder().callback(hello).build()))
.build();
No annotations, no reflection, no automatic Java-type conversion: arguments arrive as
SassValue and you cast them. In exchange the signature is explicit and
the compiler cannot silently register a method you did not mean to expose.
The @WarnFunction / @DebugFunction / @ErrorFunction annotations are replaced by
SassLogger on the options.
Importers
The SPI changed shape completely, following dart-sass:
| jsass 5 | jsass 6 |
|---|---|
Collection<Import> apply(String url, Import previous) |
URI canonicalize(String url, CanonicalizeContext ctx) and ImporterResult load(URI canonicalUrl) |
return null to skip this importer |
return null from canonicalize |
return an empty list to drop the @import |
return an ImporterResult with empty contents |
new Import(importUri, absoluteUri) to import a file |
canonicalize to the file's URI, read it in load |
new Import(importUri, absoluteUri, contents) |
return the contents from load |
previous argument |
ctx.getContainingUrl() |
The two-phase design exists so dart-sass can recognise that two different import strings refer to the same module. Give every module one canonical URI and the module system does the rest — see importers.
Errors
CompilationException with getErrorText() becomes JsassCompilationException, delivered
through the future rather than thrown:
try {
var output = compiler.compileString(scss, options).get(30, TimeUnit.SECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof JsassCompilationException sassError) {
log.error("{} at {}", sassError.getSassMessage(), sassError.getSourceSpan());
}
}
getMessage() is deliberately sanitized — no absolute paths, no engine internals. The structured
getters carry the detail. Error handling has the whole picture.
Your stylesheets
Mostly untouched, with two things to look at:
@importis deprecated in dart-sass in favour of@use/@forward. It still works and jsass can silence the warning, but the migration is worth doing — the officialsass-migratorautomates it.- libsass-era edge cases — dart-sass is stricter about
!default, division with/and unit arithmetic. Compile once withverbose(true)and read what it tells you.