npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@azerothjs/compiler

v2.1.0

Published

AzerothJS compiler - .azeroth single-file-component compiler (markup to h() calls) and Vite plugin

Readme

@azerothjs/compiler

The .azeroth single-file-component compiler (markup to h() calls) and the azeroth() Vite plugin.

npm License: MIT Node >= 22


Part of AzerothJS - the fine-grained fullstack framework. This is the build-time half: applications install azerothjs as a dependency and this compiler (with its Vite plugin) as a dev dependency.


Overview

The .azeroth single-file-component compiler. A .azeroth file is a TypeScript module written with component blocks and AzerothJS markup. The compiler turns each component into one mode-aware runtime artifact and copies everything outside a component (imports, types, helpers) through unchanged.


Install

[!NOTE] ESM-only, Node >= 22. typescript (>=5 <7) is a required peer - it parses and type-checks component expressions. vite (>=8) is an optional peer, loaded only at transform time; install it to use the azeroth() plugin (a Vite project already has it):

npm install -D @azerothjs/compiler typescript

Quickstart

[!TIP] The supported way to use it is the Vite plugin - add azeroth() to a Vite config and imports of .azeroth files just work, with source maps back to the original markup and build-time lint and diagnostics.

// vite.config.ts
import { defineConfig } from 'vite';
import { azeroth } from '@azerothjs/compiler';

export default defineConfig({ plugins: [azeroth()] });

Architecture

.azeroth is component-only - there is no standalone markup transform. The pipeline, in run order:

  1. parser - splits the source into component declarations and pass-through (opaque) regions, and parses each component body into items (props/state/derived/effect/markup output). Inner JS/TS is left as source spans.
  2. analyze - for each component, finds the reactive sources and the dependency set every expression reads, using the TypeScript parser for scope-correct read collection.
  3. lower - turns the markup output plus analysis into a target-independent Render Plan IR: a static template skeleton plus a list of surgical bindings.
  4. optimize - IR -> IR passes (constant folding).
  5. codegen - emits one artifact from the IR through a single emitter. Reactive reads become getter calls and writes become setter calls. A direct write stores the value as given, a function included; a compound or update write derives the next value from the previous one.

There is one IR and one emitter. An element-rooted output emits a mode-dispatched body: a hoisted <template> cloned on the client, the same tree serialized to HTML for SSR, and adopted on hydration. Because SSR and hydration share the emitter, their comment markers line up by construction.


Public API

The supported entry point is the Vite plugin; the lower-level pieces are exported for tooling.

  • azeroth(options?) - the Vite plugin (options.extension defaults to '.azeroth'; options.typeCheck toggles build-time type checking, default true).
  • parseModule(source) - parse a .azeroth module into its AST (components + opaque regions).
  • diagnoseModule(source) - semantic diagnostics the type system can't see (inert effects, constant deriveds, setup-time event handlers, ...).
  • typeCheckModuleTS(source, options?) - real TypeScript-backed type checking of handlers and component props (options.fileName enables cross-file resolution). Used by the plugin's typeCheck option.
  • lintSource(source) / lintMarkup(node) - markup lint (duplicate attributes, lowercase event names).
  • parseMarkup(source, start) / CompileError - parse one markup region beginning at a <.
  • findMarkupStart and the scanner helpers (skipString, skipTemplate, skipBalanced, skipRegex, isIdentStart, isIdentPart, isWhitespace, comment skippers) - markup-region scanning over arbitrary JS/TS.
  • walkComponentTags(node, visit) - visit the component tags in a markup tree.
  • Source-map helpers (vlqEncode, buildLineStarts, locationFor, encodeMappings) and the markup AST types (Span, MarkupElement, ...).

Every exported symbol is documented at its definition.


Authoring idiom and reactivity

A component is a component block. Declare reactive state with state, derived values with derived, and side effects with effect, and type props with an ordinary TypeScript parameter on the component signature. You read and write state as plain variables; the compiler rewrites a read of count to count() and a write count = x / count++ to the signal's setter, so reactivity is resolved at compile time with a tiny runtime.

component Greeting(props: { name: string })
{
    <p>Hello { props.name }</p>
}

The parameter is plain TypeScript, so every natural form works: a named interface (component Greeting(props: GreetingProps)), a destructured binding with defaults (component Greeting({ name = "world" }: GreetingProps)), or an inline object type. A destructured prop stays reactive - a bare name read lowers to props.name.

Reactive keywords: effect, deferred, and the block-wrappers

The reactive vocabulary is deliberately small. Each keyword maps to one runtime primitive; everything else (resources, roots, stores, error handling, ...) stays an ordinary import + function call.

effect { ... } - runs on mount and re-runs whenever the reactive values it reads change. Purely auto-tracked (it discovers its dependencies by running its body). A with { ... } clause passes options (e.g. name) to createEffect:

effect { document.title = `${ count } now`; }
effect with { name: 'sync' } { syncToServer(data); }   // -> createEffect(fn, { name: 'sync' })

effect (deps) [(values, prev)] [with { ... }] { ... } - an explicit-dependency effect (the on primitive). It tracks exactly the listed deps (the body's other reads do not subscribe); the optional (values, prev) binds the current and previous dependency-value tuples; with { skipInitial: true } skips the mount run.

effect (count) { logServer(count); }                     // -> on([() => count()], () => { ... })
effect (a, b) (cur, prev) { diff(prev, cur); }            // current + previous value tuples
effect (count) with { skipInitial: true } { save(count); }      // skip the mount run -> on(..., { skipInitial: true })

deferred name = expr [with { ... }] - a read-only reactive value like derived, but recomputed at idle priority. Compiles to createDeferred(() => (expr), options?); reads are bare like any source.

Block-wrappers - <keyword> { ... } -> <fn>(() => { ... }), with the body reactively rewritten:

| Keyword | Runtime | Use | |---|---|---| | batch { ... } | batch | coalesce a burst of writes into one effect flush | | untrack { ... } | untrack | run without subscribing the active effect | | cleanup { ... } | onCleanup | teardown registered inside an effect (runs before re-run/dispose) | | dispose { ... } | onRootDispose | teardown for the surrounding scope (runs once on dispose) |

effect {
    const id = setInterval(tick, 1000);
    cleanup { clearInterval(id); }
}
batch { firstName = 'Ada'; lastName = 'Lovelace'; }   // -> batch(() => { setFirstName.set(...); setLastName.set(...); })

All of these work at the component top level and in nested scopes (render callbacks, IIFEs, and module-level composable functions). For hand-written .ts, the same runtime functions are imported and called directly - createEffect/on/batch/untrack/onCleanup/onRootDispose/createDeferred.

How the compiler emits each dynamic {expr} decides whether it is reactive:

  • On a host element: an event handler (onClick={...}), a function literal, a bare reference, or an array/object literal is passed through verbatim; any other expression is wrapped in a getter () => (expr) so the runtime re-applies it when the signals it reads change.
  • On a component tag: every prop is passed as a getter-object entry (Child({ get name() { return expr } })), read uniformly as props.name, so one prop contract covers user, library, and built-in components.

For conditional classes and styles the canonical authoring form is the directive: class:active={isActive} toggles one class, style:color={tone} sets one property, and both merge with a static class="..." / style="..." on the same element. Reach for the classList() / styleMap() helpers (from azerothjs) only when the whole set is computed from an object; they return a getter the renderer resolves by calling through while it is still a function, so class={classList(obj)} needs no special-casing in the compiler.

Two-way input binding follows the same pattern: bind:value={state} (and bind:checked={state}) is the canonical mirror for a form control or a value-bearing component, desugaring to the value prop plus the write-back listener/callback. Keep the explicit value={state} + onInput={...} form for inputs that transform, validate, or otherwise do more than mirror.

A whole form is the form keyword (lowers to createForm): form f = { ...initial } with { validate, onSubmit }. A FIELD reads as f.field and binds with bind:value={f.field} (the read lowers to f.values().field, the write to f.setValue('field', v)); the rest of the form API is explicit - f.errors(), f.touched(), f.submitting(), f.handleSubmit. This is the idiomatic form style; the createForm runtime primitive can also be driven by a hand-built field component when an app prefers that abstraction.

A prop-less, child-less component tag compiles to a zero-argument call (<Comp/> -> Comp()), so a component without props never declares an unused props parameter.

Event handlers (on* attributes)

An event-handler attribute is a function position: its value must be a function - the listener invoked when the event fires ((event) => void). That is the whole rule; everything else follows from it. The value is an ordinary TypeScript expression, so the authoritative check is the type system: an expression whose type is not a function is ill-typed in handler position.

The three canonical, always-correct forms:

| Form | Example | Why it is a function | | --- | --- | --- | | Reference | onClick={save}, onClick={props.onClose} | the name is the function | | Inline function | onClick={() => count++}, onClick={(e) => save(e)} | a function literal | | Factory call | onClick={makeHandler(id)} | a call that returns a function |

Independently of types, the compiler rejects at build time any handler expression that is recognizably a side effect performed when the component is created, not a function. Exactly three forms are rejected:

  1. an assignment - count = 1, x += y (any assignment operator);
  2. an increment / decrement - count++, --n;
  3. a no-argument call of a plain name or path - save(), props.onClose(), actions.reset().

Each runs once at setup and yields a non-function value, so wiring it as a listener never does what the author intended. The fix is always to make it a function - onClick={() => count++} or onClick={save}.

Why makeHandler(id) is accepted but save() is rejected. This is not "arguments make a call valid." The compiler is a conservative rejecter: it flags only what it can prove - without type information - is a setup-time mistake, and stays silent on everything else, deferring to the type system. save(), a plain name called with no arguments, is unmistakably "call save now" (the single most common handler error), so it is rejected. makeHandler(id) matches no such pattern, so the compiler does not flag it; whether it truly returns a function is then a type question. Acceptance by the compiler means "no provable error here," not "guaranteed correct" - only the type checker guarantees that.

Mental model: a handler is a function - pass one. When in doubt, write () => .... Use a factory call only when it returns a handler; a zero-argument factory (getHandler()) is syntactically indistinguishable from the save() mistake and is therefore rejected - bind it to a name first (const handler = getHandler(); ... onClick={handler}).

The style section

A file's CSS can live in the file. style { ... } is a module-level section holding plain CSS:

style
{
    .card { padding: 1rem; border-radius: .5rem; }
    .card:hover { background: var(--hover); }
}

export default component Card
{
    <article class="card">...</article>
}

The compiler rewrites .card in the CSS and class="card" in the markup to the same content-hashed name, so two components can both define .card without colliding. That single rewrite is the entire reason the section exists - it needs to see both languages at once, which neither of them can do alone. The body is never parsed as CSS, so nesting, @media, @layer, @supports and @keyframes all work.

  • One section per file, at module level: a stylesheet belongs to the file, not to an instance.
  • Only STATIC class syntax is rewritten - class="a b" per token, and class:name={cond}. class={expr} and classList({ ... }) are TypeScript values, so they are left alone; use the css template, which returns the scoped names, when the class is computed.
  • A class the section does not define is untouched, so global stylesheets and utility frameworks such as Tailwind keep working exactly as before.
  • Element, id and attribute selectors stay GLOBAL: div { margin: 0 } applies page-wide.
  • @keyframes names are global too - only .class selectors are rewritten, so a keyframes declaration and every animation reference to it always agree, and two files defining @keyframes spin share one name (the last-loaded definition wins, per the cascade). Prefix the name with the component's class when that matters.
  • The compiler never validates the CSS. A typo inside the section reaches the browser, whose own error recovery drops the invalid declaration or rule - exactly as in a .css file. The one compile error is an unclosed section brace.
  • style remains an ordinary identifier everywhere else - el.style, const style = ..., and the style="..." attribute are unaffected.

Nothing here replaces a real stylesheet. import './card.css' at module level always worked, and a component whose CSS outgrows a section should use one.

TypeScript syntax (the TSX rules)

The full normative grammar - lexical rules, every disambiguation, the contextual keyword table, the with { } clause, and the explicit non-goals - lives in GRAMMAR.md.

Because .azeroth mixes markup with TypeScript, it follows the same disambiguation rules as .tsx:

  • No angle-bracket type assertions. Write value as Foo, not <Foo>value - a <Foo>... is read as a markup element, so a forgotten </Foo> is reported as an unclosed tag where it sits rather than silently compiling as a cast.
  • Generic arrows need a trailing comma. Write <T,>(v: T) => v; the comma tells the parser the <T,> is a type-parameter list, not a tag. Type arguments in call position (foo<Bar>(x), new C<Bar>()) are unaffected.

Precise boundary (for tooling authors): after stripping wrapping parentheses, a handler is rejected iff it is an assignment expression, a prefix/postfix ++/--, or a zero-argument call (including ?.()) whose callee is an identifier or a dotted member path. Every other expression - a reference, a function literal, a call with arguments, or a call whose callee is itself a call or an index access - is left to the type system.

A declaration value or a with { } clause may hold markup inside brackets - a parenthesised or block arrow body, a parenthesised ternary branch, an array or object literal, a call argument - where it is compiled like markup in a hole, and the markup rules that walk embedded markup apply there as they do in markup position.


Syntax highlighting anywhere

The TextMate grammar ships WITH this package - one canonical copy, welded by test to the editor bundles:

import { createHighlighter } from 'shiki';
import azeroth from '@azerothjs/compiler/azeroth.tmLanguage.json' with { type: 'json' };

const highlighter = await createHighlighter({
    themes: ['github-dark'],
    langs: [{ ...azeroth, name: 'azeroth' }]
});
highlighter.codeToHtml(source, { lang: 'azeroth', theme: 'github-dark' });

Any TextMate-compatible consumer (Shiki, docs generators, Sublime/TextMate themselves) can load the same file.


Type checking and diagnostics

Three independent layers run during a build, in increasing depth.

Lint (lintSource) - markup nits that need no type knowledge: duplicate attributes, lowercase event names. Reported as warnings.

Semantic diagnostics (diagnoseModule) - mistakes the parser can see but the type system cannot: an effect or derived with no reactive dependency (warning), a duplicate props block or a write to a read-only derived (error). Error-severity diagnostics fail the build.

Unused imports (diagnoseUnusedImports(source, compiledJs)) - an azeroth/unused-import warning for any import never used. Detection is reliable because it walks the COMPILED JS (markup already lowered to h()/component calls) for value use, then cross-checks the source so a type-only import (import type { T }, used in a props annotation component C(props: { x: T })) is never falsely flagged. Runs in the Vite plugin (which has the compiled output); warning severity, located at the unused name.

Type checking (typeCheckModuleTS) - a real TypeScript-backed checker, on by default (disable with azeroth({ typeCheck: false })). It projects each .azeroth module to TypeScript, runs the real TypeScript TypeChecker, and fails the build on a genuine type error, located back in the .azeroth source:

  • a non-function event handler - onClick={count} where count is a number (exactly the type-only mistake the handler rule above defers to the type system);
  • a wrong-typed component prop - <Child count={"x"} /> where count: number;
  • a missing required prop - <Child /> when Child requires count. A component that takes its children as markup (<Card>...</Card>) satisfies a required children prop automatically;
  • a misused built-in control-flow component - the built-ins (For, Show, Switch, ...) are checked against their real runtime prop types, so a <For> missing its required key (a runtime crash) or with a non-iterable each is caught.

Component prop checks resolve across files: props on a component imported from another .azeroth file are checked through TypeScript's module resolver (the import may use the explicit .azeroth extension or omit it). on* props on a component are typed by the receiving component, not as DOM events. The check is sound - it never reports a false error. The deliberate flip side: markup CHILDREN are not checked against a component's declared children type (a wrong-shaped child is a known false negative; render-callback children like <For>'s row function ARE typed) - modeling every runtime child shape exactly would risk the false positives this layer promises never to produce. It is enabled by default; turn it off with typeCheck: false if you want to skip it. The (immutable) TypeScript lib files are parsed once and reused across files; createIncrementalChecker binds the lib a single time and reuses one checker across every file in a build.


Related

  • AzerothJS - the monorepo overview and the full package list.
  • azerothjs - the framework runtime the compiled output targets.
  • @azerothjs/kit - per-route SSR, prerendering, and hydration for fullstack apps.