Writing a plugin
Jess does not (yet) have a formally-specified plugin API. This page documents the de-facto shape that the built-in plugins actually use, so you can write one in the same style. Two capabilities are covered:
- Registering a language / dialect — teach the compiler to handle a file extension.
- Extending parsing and resolution — bring a parser, register functions, and customize how imports are found.
This page intentionally covers the public language, parser, resolution, and
lifecycle surface. Core has no generic legacy-tree visitor hook. The deprecated
Less 4.x visitor ABI, where needed, is owned exclusively by
@jesscss/plugin-less-compat; it is not a Jess plugin interface or AST-v2
extension API.
The shape of a plugin
A plugin is an object that implements the compiler's plugin interface. The built-in
plugins extend AbstractPlugin from @jesscss/core (which supplies sensible
default resolve / getSource implementations) and are exported as a small factory
function. You pass instances to the compiler through the plugins option:
import { Compiler } from 'jess';
import lessPlugin from '@jesscss/plugin-less';
const compiler = new Compiler({
compile: {
plugins: [lessPlugin()]
}
});
The fields and methods the compiler looks for:
| Member | Purpose |
|---|---|
name | Identifier for the plugin, e.g. 'less'. |
supportedExtensions | Extensions this plugin can parse, e.g. ['.less']. |
safeParse(filePath, source, opts) | Parse a file once, returning a canonical document result plus diagnostics (never throws). |
expandImport(importPath, currentDir) | Turn an import specifier into candidate paths. |
resolve(path, currentDir, searchPaths) | Map a specifier to absolute path(s). |
locate(candidates, currentDir) | Pick the first candidate that exists. |
getSource(absolutePath) | Read a file's source (defaults to reading from disk). |
1. Registering a language / dialect
The built-in @jesscss/plugin-jess owns .jess and is registered by Compiler
by default. A custom dialect uses the same explicit extension ownership:
To actually compile that language, a plugin declares which extensions it owns and
provides a safeParse method. The compiler selects a plugin for a file by matching
supportedExtensions against the file's extension:
import { AbstractPlugin } from '@jesscss/core';
export class MyPlugin extends AbstractPlugin {
name = 'my-lang';
supportedExtensions = ['.mylang'];
safeParse(filePath: string, source: string) {
// ...parse once through the dialect grammar, returning a Stylesheet and diagnostics
}
}
safeParse returns a result object rather than throwing. Its document is the
canonical AST v2 Stylesheet (omit it when parsing failed):
document— the parsedStylesheetroot.errors— an array of error diagnostics; a non-empty array aborts compilation of the file.warnings— non-fatal diagnostics (for example deprecation notices).
Older plugin examples used { tree: Rules, errors, warnings }. That is legacy
tree terminology, not the AST v2 contract, and must not be copied into a new
plugin or adapter. Context remains the dispatcher that calls the installed
plugin's parse and import-resolution methods; this change does not create a
second loader or resolver path.
2. Extending parsing and resolution
The Less and SCSS plugins are the full-featured examples. Each one:
The public direction for every dialect, including Less, is direct Parseman
grammar reduction to Stylesheet through its package parse() operation. A
CST may still be exposed for explicit language-service/document use, but it is
not an AST bridge or a production parser route.
That is the important boundary for dialect plugins: a language front end owns syntax and import conventions, then hands the compiler the same canonical stylesheet document every other dialect hands it. Jess is designed as a common stylesheet runtime in that sense, not as a set of parallel compilers with parallel output semantics.
Customizes import resolution. expandImport turns a bare specifier into the
candidate filenames to try (e.g. foo → ./foo.less), and resolve maps
specifiers to concrete paths. A plugin whose sole job is resolution can implement
just these — @jesscss/plugin-node-modules implements import() to load npm
packages and is composed alongside a language plugin:
const compiler = new Compiler({
compile: {
plugins: [lessPlugin(), nodeModulesPlugin()]
}
});
Plugins are consulted in order, so a resolution-only plugin can extend how another plugin's language finds its imports.
Options
Give your factory an options object and fold unset values from a single defaults
object, so the CLI and the programmatic API can share one source of truth (the Less
plugin exports lessPluginDefaults for exactly this reason):
const myPlugin = (opts = {}) => new MyPlugin(opts);
export default myPlugin;
Bundler integration
Bundler plugins are thin wrappers around the compiler rather than compiler plugins.
rollup-plugin-jess, for example, creates a Compiler and calls
compiler.renderString() in Rollup's transform hook — a useful pattern if you
need to integrate Jess with another build tool.