Skip to content

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 compiler = new Compiler();
var options = new Options();
options.setOutputStyle(OutputStyle.COMPRESSED);

try {
  var output = compiler.compileString(source, options);
  System.out.println(output.getCss());
} catch (CompilationException e) {
  System.err.println(e.getErrorText());
}
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:

  1. You choose an engine. new Compiler() is gone; a compiler is built from an engine binding plus a module resolver. See getting started.
  2. Compiles are asynchronous. Every call returns a CompletableFuture<Output> and is bounded by a timeout.
  3. Options are immutable. A Lombok builder replaces the setter-per-option object, and one type (StringOptions) covers both string and file compiles.
  4. The compiler is a resource. It is AutoCloseable and 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:

jsass 5
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:

jsass 6
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:

  • @import is 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 official sass-migrator automates it.
  • libsass-era edge cases — dart-sass is stricter about !default, division with / and unit arithmetic. Compile once with verbose(true) and read what it tells you.