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

@pithyjs/compiler

v0.1.0-beta.0

Published

Parser and transformer for PithyJS `.pithy` single-file components. Converts templates into an intermediate AST and generates executable JavaScript modules.

Readme

@pithyjs/compiler

Parser and transformer for PithyJS .pithy single-file components. Converts templates into an intermediate AST and generates executable JavaScript modules.

Installation

npm install @pithyjs/compiler
pnpm add @pithyjs/compiler
yarn add @pithyjs/compiler

Overview

The compiler provides three main capabilities:

  1. Parsing — Parse .pithy single-file components and extract <template>, <script>, and <style> blocks.
  2. AST Transformation — Convert parse5 DOM trees into the PithyNode AST used by the runtime.
  3. Module Generation — Compile parsed .pithy files into ES modules that export PithyJS components.

The .pithy file format

A .pithy single-file component has three optional blocks — <template>, <script>, and <style>:

<template>
  <button @click="increment" class="btn" [class.active]="count() > 0">+</button>
  <p>count: {{ count() }}</p>
</template>

<script lang="ts">
  import { signal } from "@pithyjs/signals";

  const count = signal(0);
  const increment = () => count.set(count() + 1);
</script>

<style>
  .btn { color: red; }
</style>
  • <template> — HTML with {{ }} interpolations and directives (@click, [class.active], @if, @for, …), same as html\`` templates.
  • <script lang="ts"> — component logic. Every top-level const/let/var/function/class binding it declares — and every import — is automatically available to the template, no manual wiring. (Other top-level forms such as enum/namespace aren't auto-collected; access those through a const alias.)
  • <style> — scoped to the component (a per-file scope id is generated and applied to the template + injected CSS).

Each file compiles to a module whose default export is the component factory:

export default function Counter() {
  /* returns { el, destroy } */
}

Using .pithy files in an app

.pithy files are transformed by pithyPlugin() from @pithyjs/vite-plugin:

// vite.config.ts
import { pithyPlugin } from "@pithyjs/vite-plugin";
export default defineConfig({ plugins: [pithyPlugin() /* … */] });

Add a module declaration so TypeScript understands .pithy imports:

// vite-env.d.ts
declare module "*.pithy" {
  const component: (props?: Record<string, unknown>) => {
    el: HTMLElement | DocumentFragment;
    destroy: () => void;
  };
  export default component;
}

Then import and render it like any other component:

import Counter from "./Counter.pithy";
// use <Counter></Counter> inside an html`` template

HMR: editing a .pithy file invalidates its module and reloads the component. Signal state resets on reload — an accepted 0.1 limitation.

Usage

Parsing a .pithy file

import { parsePithyFile } from '@pithyjs/compiler';

const result = parsePithyFile(`
  <template>
    <button @click="increment()">Count: {{ count() }}</button>
  </template>
  <script lang="ts">
    import { signal } from '@pithyjs/signals';
    const count = signal(0);
    const increment = () => count.set(n => n + 1);
  </script>
  <style scoped>
    button { color: var(--color-primary); }
  </style>
`);

console.log(result.template);     // '<button @click="increment()">...'
console.log(result.script);       // { content: '...', lang: 'ts' }
console.log(result.style);        // { content: '...', lang: 'css', scoped: true }
console.log(result.identifiers);  // ['increment', 'count']

Extracting directives from attributes

import { extractDirectivesFromAttrs } from '@pithyjs/compiler';

const directives = extractDirectivesFromAttrs({
  '@click': 'handleClick()',
  '@if': 'isVisible()',
  '[disabled]': '!enabled()',
  '[class.active]': 'isActive()',
});

console.log(directives.events);   // { click: 'handleClick()' }
console.log(directives.if);       // 'isVisible()'
console.log(directives.bound);    // { disabled: '!enabled()' }
console.log(directives.classMap); // { active: 'isActive()' }

Compiling to a module

import { parsePithyFile, compileToModule } from '@pithyjs/compiler';

const sourceCode = `
  <template>
    <button @click="increment()">Count: {{ count() }}</button>
  </template>
  <script lang="ts">
    import { signal } from '@pithyjs/signals';
    const count = signal(0);
    const increment = () => count.set(n => n + 1);
  </script>
`;

const parsed = parsePithyFile(sourceCode);
const moduleCode = compileToModule(parsed, { filename: 'Counter.pithy' });
// Produces an ES module string that exports a PithyJS component function

API Reference

| API | Signature | Stability | Description | | --- | --- | --- | --- | | parsePithyFile | (code: string) => ParsedPithyFile | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | extractDirectivesFromAttrs | (attrs: Record<string, string>) => ParsedDirectives | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | forEachDirectiveAttr | (d: ParsedDirectives, cb: (attr: string, value: string) => void) => void | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | collectIfChain | (nodes: PithyNode[], startIndex: number) => PithyNode[] | stable | - | | isDefaultCase | (node: PithyNode) => boolean | stable | - | | isStructuralNode | (node: PithyNode) => boolean | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | parse5ToPithyTree | (node: Parse5Node, importedIdentifiers?: Set<string>, tagNameCasingMap: Map<number, string>, parentId?: string, attributeNameCasingMap?: Map<string, string>, source?: string) => PithyNode | null | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | ParseFragmentFn | (html: string, options?: { sourceCodeLocationInfo?: boolean }) => { childNodes: unknown[] } | experimental | - | | parseTemplate | (src: string, parseFragment: ParseFragmentFn) => { nodes: PithyNode[] } | experimental | - | | expandSelfClosingComponents | (src: string) => { result: string; insertions: Array<{ at: number; len: number }> } | experimental | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | extractOriginalTagNames | (template: string) => Map<number, string> | stable | - | | extractOriginalAttributeNames | (template: string) => Map<string, string> | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | compileToModule | (parsed: ParsedPithyFile, options?: { filename?: string; scopeId?: string }) => string | stable | - |

| API | Signature | Stability | Description | | --- | --- | --- | --- | | warnOnLowercaseComponentTags | (template: string, importedIdentifiers: Set<string>) => void | stable | - |

Examples

should handle @routerOutlet (camelCase)

const attrs = { '@routerOutlet': '' };
const directives = extractDirectivesFromAttrs(attrs);
directives.routerOutlet; // → true

should handle @if directive

const attrs = { '@if': 'condition()' };
const directives = extractDirectivesFromAttrs(attrs);
directives.if; // → 'condition()'

should handle @for directive

const attrs = { '@for': '(item, i) of items' };
const directives = extractDirectivesFromAttrs(attrs);
directives.for; // → defined
directives.for?.itemVar; // → 'item'
directives.for?.indexVar; // → 'i'
directives.for?.collectionExpr; // → 'items'

should handle @defer directive with visible

const attrs = { '@defer': 'visible' };
const directives = extractDirectivesFromAttrs(attrs);
directives.defer; // → 'visible'

should extract @click as event

const attrs = { '@click': 'handleClick()' };
const directives = extractDirectivesFromAttrs(attrs);
directives.events?.click; // → 'handleClick()'

should handle [attr] syntax

const attrs = { '[disabled]': '!enabled()' };
const directives = extractDirectivesFromAttrs(attrs);
directives.bound?.disabled; // → '!enabled()'

should handle @ref directive

const attrs = { '@ref': 'inputRef' };
const directives = extractDirectivesFromAttrs(attrs);
directives.ref; // → 'inputRef'

should handle @error directive

const attrs = { '@error': 'errorSignal' };
const directives = extractDirectivesFromAttrs(attrs);
directives.error; // → 'errorSignal'
import { forEachDirectiveAttr } from '@pithyjs/compiler';

forEachDirectiveAttr(node.directives, (attr, value) => {
  el.setAttribute(attr, value);
});
// Sets: data-on-click="handler()", data-ref="myRef", etc.
// Given: [div@if, span@else-if, p@else, footer]
collectIfChain(nodes, 0) // → [div@if, span@else-if, p@else]
isDefaultCase({ type: 'element', attrs: { '@default': '' } }) // → true
isDefaultCase({ type: 'element', attrs: { class: 'box' } })   // → false
isStructuralNode({ type: 'element', directives: { if: 'show()' } })  // → true
isStructuralNode({ type: 'element', attrs: { '@default': '' } })     // → true
isStructuralNode({ type: 'element', directives: { ref: 'myRef' } })  // → false
isStructuralNode({ type: 'text', textContent: 'hello' })             // → false

Architecture

The compiler processes .pithy files through three layers:

.pithy source → parsePithyFile() → { template, script, style }
                                          ↓
template string → parse5 → parse5ToPithyTree() → PithyNode AST
                                                      ↓
{ parsed } → compileToModule() → ES module string

Key types

  • PithyNode — Core AST node (element, text, or slot)
  • ParsedDirectives — All recognized directives from element attributes
  • ParsedPithyFile — Result of parsing a .pithy file
  • InterpolationInfo — Metadata for {{ expression }} interpolations

Directive mapping

The forEachDirectiveAttr() function is the single source of truth for mapping ParsedDirectives fields to data-* attributes used by the runtime binders.

Testing

| Feature | Unit | Integration | E2E | Status | | --- | --- | --- | --- | --- | | extractDirectivesFromAttrs | - | - | - | Missing unit, integration |

cd packages/compiler && pnpm test

License

MIT