myavka
v1.2.2
Published
CSS preprocessor
Maintainers
Readme
Myavka CSS Preprocessor
What is this
myavka is a small, lightweight CSS preprocessor with a module system built around a real dependency graph instead of textual substitution — modules are resolved and deduplicated properly, not copy-pasted wherever they're referenced.
Advantages
- Modularity. Styles are organized as small, self-contained, reusable modules instead of one flat cascade you have to reason about as a whole.
- Encapsulation. Modules don't leak into a shared global namespace — applying a module can't accidentally collide with or override something unrelated elsewhere in the project.
- A real import/export system. Modules are explicitly imported and exported between files, closer to how JS modules work, instead of Sass-style implicit, order-dependent inclusion.
- Built-in file watching. Tracks source file changes out of the box — no extra tooling needed to rebuild on edit.
- Simple, focused set of directives. A small, deliberate instruction set rather than a sprawling language with loops, conditionals, and imperative-feeling variables.
- Friendly to any valid CSS. Plain CSS files pass through untouched — you can drop myavka into an existing codebase without rewriting anything.
Why this exists
Why not Sass. Sass placeholders live in one shared, global namespace — there's no real encapsulation, just naming discipline you have to maintain by hand. @extend and nested @use/@import behave in ways that are hard to predict from reading the source alone, and the language itself is imperative in spirit (loops, conditionals, mutable-feeling variables) grafted onto a fundamentally declarative format. It works, but it doesn't feel like it was designed for the problem it's solving.
Why not PostCSS. PostCSS itself is just a transform pipeline, not a language — and the plugin ecosystem built on top of it is wildly uneven. Import/export isn't a coherent, built-in concept; you assemble it from third-party plugins, many of which are half-finished, disagree with each other on edge cases, and don't handle even obvious cases like deduplicating repeated output correctly. You end up integrating a pile of plugins instead of using a language.
Installation
npm install myavkaimport { compile, compileString } from 'myavka';
const result = await compile('styles/main.css');
result.css; // compiled CSS as a string
await result.save('dist/main.css');compileString works the same way, but takes source text directly instead of a file path:
const result = await compileString('%button { padding: 8px; } .btn { @apply %button; }');
result.css;watch compiles a file and keeps recompiling it whenever it (or any of its dependencies) changes:
import { watch } from 'myavka';
const watcher = await watch('styles/main.css');
watcher.on('ok', (result) => {
result.save('dist/main.css');
});
watcher.on('fail', (error) => {
console.error(error);
});
// later
watcher.unwatch();API
Global methods
async compile(url, options)— compiles a file from the given URL/path.async compileString(source, options)— compiles a source string directly.async compileFile(url, options)— alias forcompile().async watch(url, options)— compiles a file and watches it and all of its dependencies, recompiling as needed. Returns aWatcher.
url is a URL (or path) to the file being compiled — supports file:, http:, https:, data:, blob:, and relative paths (resolved relative to engine.url).
options.url — for compileString(), an additional base URL used to resolve any imports inside the given source.
options.style — CSS output formatting style. Default: "pretty".
"minify""pretty""colors"— same as"pretty", plus syntax highlighting in the console.- an object for fine-grained control:
options.style.tab— indentation charactersoptions.style.newline— line break charactersoptions.style.colors—true/false, syntax highlighting
Engine (exported as Engine)
A separate, self-contained compiler instance — an alternative to the default global engine.
new Engine(options)options.url— root URL for resolving all relative paths incompile/watch. Defaults toprocess.cwd().options.gcDelay— delay (ms) after compiling all files before the file cache is garbage-collected.
engine.url— the engine's root URL.engine.resolve(url)— resolves a URL/path to its full, absolute URL.engine.compile(url, options)— same as the global method.engine.compileString(source, options)— same as the global method.engine.compileFile(url, options)— same as the global method.engine.watch(url, options)— same as the global method.engine.stats()— returns a stats object describing the engine's current state.on('link', listener(url))— fired once a module's dependencies have been resolved and its structure is fully built.on('load', listener(url, source))— fired once a source has been fetched, before parsing —sourceis the raw, unparsed text.on('parse', listener(url, snapshot))— fired once a source has been parsed into its internal representation.on('watch', listener(url))— fired when watching resumes for the given file.on('unwatch', listener(url))— fired when watching stops for the given file.on('ok', listener(result))— fired whenever a compilation finishes successfully, anywhere in the engine.on('fail', listener(error, url))— fired whenever a compilation fails, with the URL that caused it.on('gc', listener(stats))— fired after the engine's file cache is garbage-collected
Result
result.css— the compiled output as a string.result.url— the root file's URL, ornullfor strings compiled viacompileString.result.imports— full URLs of every file used during compilation.async result.save(filename)— convenience method to write the result to a file.
Watcher
Obtained from watch() / engine.watch().
- Standard emitter methods:
on/off/once/emit. watcher.watching—trueif file watching is currently active.watch()— resumes file watching if it was previously turned off.unwatch()— turns file watching off.close()— alias forunwatch().on('ok', listener(result))— fired every time the file is recompiled successfully.on('fail', listener(error))— fired on a compilation error.on('watch', listener())— fired when watching resumes for the given file.on('unwatch', listener())— fired when watching stops for the given file.
engine — the default global engine instance backing the global methods above.
Language syntax
myavka's syntax is fully compatible with plain CSS. There are only three additional directives — @import, @export, @apply — plus module blocks, written as %name { ... } (a placeholder-like construct).
Modules
Modules (placeholders) are named, reusable blocks that can be applied in different contexts. They can contain any CSS content, and can apply other modules themselves.
%x { color: red; }Declares a module — or appends to an existing one in the same scope under that name.
%x div { color: red; }Shorthand for:
%x {
div { color: red; }
}%x, %y div { color: red; }Adds the same block to both modules at once.
Creating and using a module
%button {
padding: 8px 16px;
border-radius: 4px;
}
.submit {
@apply %button;
background: blue;
}%button never shows up in the output on its own — its content only appears wherever it's applied, here as part of .submit.
Scope
Module names follow the same lexical scoping as everything else in myavka: a %name declared inside a block is only visible within that block, and shadows any module with the same name from an outer scope.
%x { ... }
%x div { }
div {
%x { }
%x div { }
@apply %x;
}
@apply %x;- Inside the
div { ... }block,%x { }declares a new, block-scoped%x— distinct from the one at the root.%x div { }right after it appends to this same, inner%x. @apply %x;inside thedivblock resolves to this inner%x— the nearest one in scope.@apply %x;after thedivblock closes is back at the root scope, where the inner%xno longer exists — it resolves to the original, root-level%x.
Nested module declarations
A module can declare other modules inside its own body:
%x {
%y { }
@apply %y;
}%y here is scoped to %x's body — it isn't visible outside of it. Applying it from within the same body is fine, since it's already in scope by the time @apply %y; is reached.
Cyclic usage
A module applying itself through a nested rule is a genuine cycle, and a compile-time error:
%x {
div {
@apply %x;
}
}A module applying itself directly, at the same level, with no additional selector nesting, is allowed but pointless — there's nothing new for the self-application to add, so it's simply ignored:
%x {
@apply %x;
}This is not a cycle either, even though it looks similar — the inner %x is a different, shadowed module (see Scope above), not a self-reference:
%x {
%x { }
div {
@apply %x;
}
}@import
@import <url>;Left untouched — passed straight through to the output as-is.
@import * from 'url';Imports every module from the given file into the current namespace.
@import %x, %y from 'url';Imports the named modules into the current namespace.
@import %x as %y from 'url';Imports the named module under a new name.
@import % as %x from 'url';Imports the file's root (if it exports one) into the current namespace. Renaming is mandatory here — you can't import the root without giving it a name.
@import %a as %b, %c, % as %e, * from 'url';All of the above can be combined. Note that when renaming (%a as %b), the original name (%a) is not also imported — not even when combined with *.
Imports can be nested, and follow ordinary lexical scoping:
div {
@import %x from 'url';
/* %x is visible here */
@apply %x;
}
/* error — %x isn't visible outside the block it was imported in */
@apply %x;@export
Declares which modules from the current file are available for import elsewhere.
@export can't be nested — it's only valid at the file's root scope.
If a file contains no @export rules at all, the file's own root is exported implicitly, and can be imported as @import % as %x from 'url';. This is what keeps plain CSS files compatible with myavka's import system without any changes.
@export *;Exports every module declared in the root scope except the root itself.
@export %;Exports only the file's root (the whole file). This is the default behavior when there are no @export rules.
@export % as %x;Exports the root under a new name.
@export %x, %y;Exports the named modules from the root scope.
@export %x as %y;Exports the named modules under new names.
@export *, %, %x as %y;All of the above can be combined. As with imports, when renaming (%x as %y), the original name (%x) is not also exported.
Re-exporting
@export ... from 'url';Exports modules sourced from another file, using the same set of variants as a regular export:
@export * from 'url';
@export % from 'url';
@export % as %x from 'url';
@export %x, %y from 'url';
@export %x as %y from 'url';
@export *, %, %x as %y from 'url';@apply
Applies one or more modules — effectively, appends the current selector to the module's content.
@apply %x, %y;Applies the named modules to the current scope.
@apply *;Applies every module currently visible in scope.
@apply %x, %y from 'url';Applies modules taken from another file — effectively @import + @apply combined. Unlike a regular import, the names aren't added to the current namespace; the modules are applied directly and nothing else.
@apply * from 'url';Applies every module in the given file's export list (except the root).
@apply % from 'url';Applies the given file's root, if it exports one.
@apply *, %;Combines the root and every visible module.
Philosophy
- File-level modularity and encapsulation. Each file only sees what's declared in that file, or explicitly imported into it.
- Scope-level modularity and encapsulation. Each scope — any block — only sees what's declared or imported above it, or directly within it.
- Nothing is global. Nothing passes transitively between files unless you explicitly import it.
- Consistency and predictability. Every construct follows the same shape as much as possible — the syntax stays uniform across the language rather than growing special cases.
- Simplicity and minimalism. A minimal, sufficient set of directives, each as flexible as possible. The smallest possible footprint on plain CSS — every integration happens at the rule level.
- Unobtrusive by default. Any rule the preprocessor doesn't recognize as its own is passed through to the output as-is, following standard CSS rules.
- Valid CSS in, correctly handled. Any valid CSS file is processed correctly by the preprocessor.
- The reverse holds too. In the common case, valid myavka output is also valid CSS.
- Compact output. The compiled result avoids duplication wherever possible.
