Importers
An importer decides what @use "…" and @import "…" actually load. jsass 6 follows the dart-sass
model, which splits the job in two:
canonicalize(url, context)turns whatever the stylesheet wrote into a canonicalURI, or returnsnullto say "not mine, ask the next one".load(canonicalUrl)returns the stylesheet contents for a URI that this importer canonicalized.
The split is what makes caching and deduplication possible: two stylesheets that canonicalize to the same URI are the same module and are loaded once.
A minimal importer
public final class MapBackedImporter implements Importer {
private static final String SCHEME = "virtual";
private final Map<String, String> modules;
public MapBackedImporter(Map<String, String> modules) {
this.modules = Map.copyOf(modules);
}
@Override
public @Nullable URI canonicalize(String url, CanonicalizeContext context) {
String name = url.startsWith(SCHEME + ":") ? url.substring(SCHEME.length() + 1) : url;
return modules.containsKey(name) ? URI.create(SCHEME + ":" + name) : null;
}
@Override
public @Nullable ImporterResult load(URI canonicalUrl) {
if (!SCHEME.equals(canonicalUrl.getScheme())) {
return null;
}
String contents = modules.get(canonicalUrl.getSchemeSpecificPart());
return contents == null ? null : DefaultImporterResult.builder()
.contents(contents)
.syntax(Syntax.SCSS)
.build();
}
}
var options = StringOptions.builder()
.url(URI.create("virtual:root.scss"))
.importers(List.of(new MapBackedImporter(Map.of("theme", "$brand: red;"))))
.build();
CanonicalizeContext tells you why you are being asked: isFromImport() distinguishes a legacy
@import from a @use, and getContainingUrl() is the stylesheet the request came from — the
hook for relative resolution.
An ImporterResult carries the contents, the syntax (SCSS by default) and an optional
sourceMapUrl. Build one with ImporterResult.builder() or DefaultImporterResult.builder().
Resolution order
For a single compile, dart-sass asks in this order:
- the importer set with
importer(…), which also owns relative loads from the entry stylesheet, - each importer from
importers(…), in list order, - the
loadPaths, which jsass appends as one last built-in importer.
The first importer returning a non-null canonical URL wins. Returning null is the normal way
to decline.
Load paths
loadPaths is the plain-filesystem case. On the JavaScript engines jsass implements it in Java
rather than delegating to dart-sass — dart-sass's own file lookup needs an API that only exists in
the Node runtime, so doing it ourselves keeps load paths working on V8 and GraalJS too. There, a URL
that escapes a load path through .. makes that load path decline, and the next one is tried.
Multiple load paths are the norm and one tainted entry should not abort the compile.
The embedded engine passes load paths to the native dart-sass as its own load paths, in the same
position after your importers. dart-sass does not confine .. to a load path, so an import can
reach files next to it. If a directory must be a hard boundary on every engine, serve it through
an importer that checks for itself.
WebJars
jsass.webjar-importer resolves imports against WebJars on the classpath, which is how you pull
Bootstrap or Foundation into a build without checking their sources in:
import io.bit3.jsass.webjar.WebjarImporter;
var options = StringOptions.builder()
.url(URI.create("webjar:style.scss"))
.importer(WebjarImporter.builder()
.allowedWebjars(Set.of("bootstrap"))
.build())
.build();
WebjarImporter lives in the package io.bit3.jsass.webjar (artifact jsass.webjar-importer,
an automatic module). It is independent of the
module resolver that loads dart-sass, and works with every engine,
the embedded one included.
| Builder property | Default | Purpose |
|---|---|---|
allowedWebjars |
empty | artifact names this importer may serve, case-insensitive |
allowClasspathScan |
false |
true allows any WebJar on the classpath |
classLoader |
the importer's own | scopes the WebJar scan and reads the stylesheets; for container setups with an isolated classpath |
charset |
UTF-8 |
encoding of the stylesheet sources |
The allowlist is empty by default
An importer with no allowedWebjars resolves nothing. That is deliberate: without it, any
WebJar that happens to be on the classpath — including one pulled in transitively — could be
reached from a stylesheet. Name the artifacts you mean, or set allowClasspathScan(true) if
you accept the wider surface.
Failure modes
| Situation | What happens |
|---|---|
| Importer declines | canonicalize returns null, the next importer is asked |
Artifact not in allowedWebjars |
declines, so another importer still gets its turn |
URL contains .. / . segments, or an opaque webjar: URI has no path |
SassPathTraversalException — unchecked, aborts the compile |
| One import matches several stylesheets | AmbiguousImportException naming every candidate (thrown as AmbiguousImportRuntimeException, which carries it) |
| Your importer throws anything else | wrapped as SassImporterExecutionException, reported as a Sass error at the import site |
SassPathTraversalException is unchecked because the SPI signature has no throws clause for it.
It carries getRequestedPath() and getAllowedRoot() for logging.
Whatever an importer throws — these two included — ends the compile: the future fails with a
SassImporterExecutionException whose getCause() is the original exception.