@pithyjs/core
v0.1.0-beta.0
Published
**Main runtime package** for PithyJS - a next-generation web framework centered on fine-grained reactivity and direct DOM updates.
Downloads
39
Readme
@pithyjs/core
Main runtime package for PithyJS - a next-generation web framework centered on fine-grained reactivity and direct DOM updates.
Overview
@pithyjs/core combines reactive signals, template compilation, and directive binding into a unified runtime. It provides:
- Fine-grained reactivity via
@pithyjs/signals - Template compilation with HTML parsing and directive binding
- Structural directives (@if, @for, @switch)
- Event handling with automatic cleanup
- Component system with scoped styles
- Zero virtual DOM - only signal-bound nodes update
Key Principle: Components never re-render. When a signal changes, only the specific DOM nodes bound to that signal update (O(1) operation).
Installation
npm install @pithyjs/corepnpm add @pithyjs/coreyarn add @pithyjs/coreQuick Start
import { html, signal } from '@pithyjs/core';
const count = signal(0);
const increment = () => count.set(count() + 1);
const app = html`
<div>
<h1>Counter: {{ count() }}</h1>
<button @click="increment()">Increment</button>
</div>
`;
document.body.appendChild(app);Core Features
Reactive Signals
import { signal, computed, effect } from '@pithyjs/core';
const name = signal('Alice');
const greeting = computed(() => `Hello, ${name()}!`);
effect(() => {
console.log(greeting()); // Logs: "Hello, Alice!"
});
name.set('Bob'); // Logs: "Hello, Bob!"Template Interpolations
const user = signal({ name: 'Alice', age: 30 });
html`
<div>
<h1>{{ user().name }}</h1>
<p>Age: {{ user().age }}</p>
</div>
`;Event Handlers
const handleClick = () => console.log('Clicked!');
const handleInput = (e) => console.log(e.target.value);
html`
<button @click="handleClick()">Click me</button>
<input @input="handleInput($event)" />
`;Conditionals
const isLoggedIn = signal(false);
html`
<div @if="isLoggedIn()">
<h1>Welcome back!</h1>
</div>
<div @else>
<h1>Please log in</h1>
</div>
`;For Loops
const todos = signal([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: true }
]);
html`
<ul>
<li @for="(todo, i) of todos()" track="todo.id">
{{ i() + 1 }}. {{ todo().text }}
<span @if="todo().done">✓</span>
</li>
</ul>
`;Complete documentation: src/compile/forLoops/README.md
Switch/Case
const status = signal('loading');
html`
<div @switch="status()">
<div @case="'loading'">Loading...</div>
<div @case="'success'">Success!</div>
<div @case="'error'">Error occurred</div>
<div @default>Unknown status</div>
</div>
`;Lazy Loading
Load components on-demand to improve initial page load performance.
import { lazy, preloadLazy } from '@pithyjs/core';
// Create a lazy-loaded component
const HeavyChart = lazy(() => import('./HeavyChart.pithy'));
// Use in template with @defer directive
html`
<div @defer="visible" @load="HeavyChart">
<div slot="placeholder">Loading chart...</div>
<div slot="error">Failed to load</div>
</div>
`;
// Preload on hover for faster perceived load
button.addEventListener('mouseenter', () => preloadLazy(HeavyChart));Trigger modes:
@defer="visible"- Load when element scrolls into viewport@defer="when" @when="condition()"- Load when condition becomes true
Complete documentation: src/runtime/README.md
Class Directives
const isActive = signal(true);
const isDisabled = signal(false);
html`
<button
[class.active]="isActive()"
[class.disabled]="isDisabled()"
>
Toggle
</button>
`;Style Directives
const color = signal('red');
const size = signal(16);
html`
<div
[style.color]="color()"
[style.font-size]="size() + 'px'"
>
Styled text
</div>
`;Architecture
@pithyjs/core
├── runtime/
│ ├── html.ts # html`` template tag
│ ├── applyBindings.ts # AOT binding applicator (Phase 5 stub)
│ ├── lazy.ts # Lazy loading (lazy(), preloadLazy())
│ ├── module-cache.ts # Module caching for lazy components
│ └── README.md # Runtime documentation
├── compile/
│ ├── compileTemplate.ts # Main compilation entry
│ ├── parseHtml.ts # HTML parser
│ ├── forLoops/ # For loop system
│ │ └── README.md # Complete @for documentation
│ └── binds/ # Directive implementations
│ ├── bindTextInterpolations.ts
│ ├── bindEventHandlers.ts
│ ├── bindClassDirectives.ts
│ └── bindConditionals.ts
└── index.ts # Public APITemplate Compilation System
The compilation system transforms templates into reactive DOM:
- Parse HTML into AST
- Bind structural directives (creates contexts):
- @if/@else-if/@else chains
- @switch/@case statements
- @for loops
- Bind non-structural directives (uses contexts):
- Text interpolations {{ }}
- Event listeners @event
- Class/style directives
Complete documentation: src/compile/README.md
For Loops System
The @for directive provides keyed, reactive list rendering with:
- O(1) signal updates when items change
- Minimal DOM moves using LIS algorithm for reordering
- Range-based architecture with comment anchors
- Deep nesting support (loops in loops, loops in conditionals)
- Automatic cleanup on unmount
Complete documentation: src/compile/forLoops/README.md
Performance Characteristics
| Operation | Complexity | Notes | |-----------|------------|-------| | Signal read | O(1) | Direct value access | | Signal write | O(1) | Updates only subscribers | | Text interpolation update | O(1) | Only bound text node updates | | Event binding | O(1) | Direct listener attachment | | Loop item add | O(1) | Keyed insertion | | Loop item remove | O(1) | Range detachment | | Loop item update | O(1) | Signal update, no re-render | | Loop reorder | O(n log n) | LIS algorithm | | Conditional toggle | O(m) | m = branch nodes |
Target: Sub-1KB core runtime (min+gzip)
API Reference
Runtime APIs
Note: This table is auto-generated from codex annotations. APIs marked
internalare not exported from the@pithyjs/corepublic barrel — they are used internally or via direct subpath imports (e.g.,@pithyjs/core/runtime/applyBindings).
| API | Component | Signature | Stability | Description |
| --- | --- | --- | --- | --- |
| compileNodes | compile-nodes | (nodes: PithyNode[], context: Record<string, unknown>) => { el: DocumentFragment; destroy: () => void } | stable | - |
| compileTemplate | renderer | (html: string, context: Record<string, any>, css?: string, scopeId?: string, importedIdentifiers?: Set<string>) => { el: HTMLElement | DocumentFragment; destroy: () => void } | stable | - |
| bindTemplateDirectives | renderer | (fragment: DocumentFragment, context: Record<string, any>, disposers: (() => void)[]) => void | stable | - |
| isSSRMode | ssr-mode | () => boolean | stable | - |
| toTextValue | dom-safety | (input: unknown) => string | internal | - |
| __applyBindings | apply-bindings | (fragment: DocumentFragment, bindings: Desc[], context: Record<string, unknown>) => { el: DocumentFragment; destroy: () => void } | experimental | - |
| bootstrap | bootstrap | (options: BootstrapOptions) => Promise<void> | stable | - |
| createContextProxy | context | (contexts: Record<string, unknown>[]) => Record<string, unknown> | stable | - |
| loadWithTimeout | defer-helpers | <T>(handle: LazyHandle<T>, timeoutMs: number) => Promise<T> | internal | - |
| scheduleByPriority | defer-helpers | (callback: () => void, priority: LoadingPriority | undefined) => number | void | internal | - |
| html | html | (template: TemplateStringsArray | string, contextOrCss?: Record<string, unknown> | string, cssIfContextPassed?: string) => { el: HTMLElement | DocumentFragment; destroy: () => void } | stable | - |
| onDestroy | onDestroy | (fn: () => void) => void | stable | - |
| bindBoundAttributes | attributes | (root: Element | DocumentFragment, context: Record<string, unknown>, disposers: (() => void)[]) => void | stable | - |
| bindClassDirectives | classes | (root: Element | DocumentFragment, ctx: Record<string, unknown>, disposers: (() => void)[]) => void | stable | - |
| bindComponentTags | components | (root: Element | DocumentFragment, rootCtx: Record<string, unknown>, disposers: (() => void)[]) => void | stable | - |
| bindDefaultTwoWay | two-way-binding | (fragment: Element | DocumentFragment, context: Record<string, unknown>, disposers: (() => void)[]) => void | stable | - |
| bindEventHandlers | events | (root: Element | DocumentFragment, rootCtx: Record<string, unknown>, disposers: (() => void)[]) => void | stable | - |
| bindTextInterpolations | interpolations | (root: Element | DocumentFragment, ctx: Record<string, unknown>, disposers: (() => void)[]) => void | stable | - |
| buildPidMap | binding-helpers | (fragment: DocumentFragment) => Map<string, Element> | internal | - |
| buildCommentMap | binding-helpers | (fragment: DocumentFragment) => Map<string, Comment> | internal | - |
| nearestCtx | binding-helpers | (el: Node, rootCtx: BindingContext) => BindingContext | internal | - |
| mergeContexts | binding-helpers | (context: BindingContext) => BindingContext | internal | - |
| compileWithEvent | binding-helpers | (expr: string, ctx: BindingContext) => (ev: Event, el: Element) => unknown | internal | - |
Directive APIs
| API | Component | Signature | Stability | Description |
| --- | --- | --- | --- | --- |
| bindSwitchCases | switch | (nodes: PithyNode[], rootEl: HTMLElement | DocumentFragment, ctx: Record<string, unknown>, processedNodeIds?: Set<string>, disposers?: (() => void)[]) => void | stable | - |
| handleConditionalChain | conditional-chain | (domChain: HTMLElement[], astChain: PithyNode[], ctx: Record<string, unknown>, processedNodeIds?: Set<string>, disposers?: (() => void)[]) => Set<string> | stable | - |
| bindConditionals | if | (nodes: PithyNode[], rootEl: HTMLElement | DocumentFragment, ctx: Record<string, unknown>, processedNodeIds?: Set<string>, disposers?: (() => void)[]) => Set<string> | stable | - |
| bindForLoops | for | (nodes: PithyNode[], rootEl: HTMLElement | DocumentFragment, ctx: Record<string, unknown>, disposers?: (() => void)[], processedNodeIds?: Set<string>) => void | stable | - |
| createLoopManager | loop-manager | <T>(options: LoopManagerOptions<T>) => LoopManager<T> | stable | - |
| safeInsertBefore | loop-helpers | (parent: Node, newNode: Node, referenceNode: Node | null) => void | stable | Safely inserts a node before a reference node, with fallback to appendChild |
| markElementsAsLoopGenerated | loop-helpers | (fragment: DocumentFragment, node: { nodeId?: string }) => void | stable | Marks all elements in a fragment as loop-generated for safe cleanup Optimized to use targeted selectors when possible |
| createItemRange | loop-helpers | (key: string) => { start: Comment; end: Comment } | stable | - |
| wrapInRange | loop-helpers | (start: Comment, content: DocumentFragment | HTMLElement, end: Comment) => DocumentFragment | stable | - |
| detachRange | loop-helpers | (start: Comment, end: Comment) => void | stable | - |
| createLoopSignals | loop-signals | <T>(item: T, idx: number, total: number) => LoopSignals | stable | Creates all the reactive signals needed for a loop item |
| updateLoopSignals | loop-signals | (signals: LoopSignals, idx: number, total: number) => void | stable | Updates all positional signals for a loop item (idx, first, last, even, odd, count) Uses batch() for optimal performance |
| createLoopContext | loop-signals | (parentCtx: Record<string, unknown>, signals: LoopSignals, itemVar: string, indexVar?: string, countVar?: string, firstVar?: string, lastVar?: string, evenVar?: string, oddVar?: string) => Record<string, unknown> | stable | Creates the context object for a loop item with all necessary signals. Helper signal names (count, first, last, even, odd) can be aliased to avoid shadowing in nested loops. |
| findAllNestedLoopNodes | loop-traversal | (node: PithyNode) => PithyNode[] | stable | Recursively finds all nested loop nodes within a given node Used to process nested loops before binding to preserve data-pid references |
Lazy Loading APIs
| API | Signature | Stability | Description |
| --- | --- | --- | --- |
| LazyState | type LazyState = 'idle' | 'loading' | 'loaded' | 'error' | stable | Possible states for a lazy component |
| LazyOptions | interface LazyOptions | stable | Options for configuring lazy loading behavior |
| LazyHandle | interface LazyHandle<T> | stable | Handle returned by lazy() for controlling lazy component loading |
| lazy | <T>(loader: () => Promise<T>, options?: LazyOptions) => LazyHandle<T> | stable | - |
| isLazyHandle | (value: unknown) => value is LazyHandle<unknown> | stable | - |
| preloadLazy | <T>(handle: LazyHandle<T>) => Promise<T> | stable | - |
Accessibility APIs
| API | Component | Signature | Stability | Description |
| --- | --- | --- | --- | --- |
| FocusTrap | focus-trap | class FocusTrap | stable | Focus Trap Instance Manages focus containment within an element for accessible modals/dialogs |
| activate | focus-trap | activate(): void | stable | - |
| deactivate | focus-trap | deactivate(): void | stable | - |
| updateFocusableElements | focus-trap | updateFocusableElements(): void | stable | - |
| isActivated | focus-trap | isActivated(): boolean | stable | - |
| destroy | focus-trap | destroy(): void | stable | - |
| createFocusTrap | focus-trap | (element: HTMLElement, options?: FocusTrapOptions) => FocusTrap | stable | Create a focus trap for an element Convenience function for creating and activating a focus trap |
| isKey | keyboard-utils | (event: KeyboardEvent, key: string) => boolean | stable | Check if a keyboard event matches a specific key |
| isActivationKey | keyboard-utils | (event: KeyboardEvent) => boolean | stable | Check if keyboard event is an activation key (Enter or Space) Used for custom interactive elements (buttons, links, etc.) |
| onEscape | keyboard-utils | (handler: KeyboardHandler, options?: KeyboardHandlerOptions) => () => void | stable | Handle Escape key press Commonly used to close modals, dropdowns, tooltips, etc. |
| onEnter | keyboard-utils | (element: HTMLElement, handler: KeyboardHandler, options?: KeyboardHandlerOptions) => () => void | stable | Handle Enter key press |
| onSpace | keyboard-utils | (element: HTMLElement, handler: KeyboardHandler, options?: KeyboardHandlerOptions) => () => void | stable | Handle Space key press Note: Space typically triggers on keydown for consistency with Enter |
| onActivation | keyboard-utils | (element: HTMLElement, handler: KeyboardHandler, options?: KeyboardHandlerOptions) => () => void | stable | Handle activation keys (Enter or Space) Used for custom buttons, links, and other interactive elements |
| onArrowKeys | keyboard-utils | (element: HTMLElement, handlers: ArrowKeyHandlers, options?: KeyboardHandlerOptions) => () => void | stable | Handle arrow key navigation |
| hasModifierKeys | keyboard-utils | (event: KeyboardEvent) => boolean | stable | Check if modifier keys are pressed Modifier keys: Ctrl, Alt, Shift, Meta (Command/Windows key) |
| onKeyboardShortcut | keyboard-utils | (shortcut: string, handler: KeyboardHandler, options?: KeyboardHandlerOptions) => () => void | stable | Create a keyboard shortcut handler Supports combinations like "Ctrl+S", "Cmd+K", "Escape", etc. |
| makeKeyboardAccessible | keyboard-utils | (element: HTMLElement, handler: KeyboardHandler) => () => void | stable | Make an element keyboard-accessible by adding Enter/Space key handlers Useful for making divs/spans behave like buttons |
| RovingTabindex | roving-tabindex | class RovingTabindex | stable | Roving Tabindex Manager Implements the roving tabindex pattern for keyboard navigation in custom widgets Used for tabs, menus, toolbars, listboxes, etc. |
| focusItem | roving-tabindex | focusItem(index: number): void | stable | - |
| updateItems | roving-tabindex | updateItems(items: HTMLElement[] | NodeListOf<HTMLElement>): void | stable | - |
| getCurrentIndex | roving-tabindex | getCurrentIndex(): number | stable | - |
| getCurrentItem | roving-tabindex | getCurrentItem(): HTMLElement | null | stable | - |
| destroy | roving-tabindex | destroy(): void | stable | - |
| createRovingTabindex | roving-tabindex | (items: HTMLElement[] | NodeListOf<HTMLElement>, options?: RovingTabindexOptions) => RovingTabindex | stable | Create a roving tabindex manager Convenience function for creating a roving tabindex |
| SkipLink | skip-link | (options?: SkipLinkOptions) => SkipLinkResult | stable | Skip Link Component Provides a keyboard-accessible way to bypass repeated content Meets WCAG 2.4.1 Level A requirement |
| SkipLinks | skip-link | (links: SkipLinkOptions[]) => SkipLinkResult | stable | Create multiple skip links for different page sections |
Template Syntax
| Syntax | Purpose | Example |
|--------|---------|---------|
| {{ expr }} | Text interpolation | {{ count() }} |
| @event | Event binding | @click="handler()" |
| @if / @else-if / @else | Conditional rendering | @if="show()" |
| @for | Loop rendering | @for="(item, i) of items()" track="item.id" |
| @switch / @case / @default | Multi-way branch | @switch="value()" |
| @defer / @load | Lazy loading | @defer="visible" @load="LazyComponent" |
| [class.name] | Class toggle | [class.active]="isActive()" |
| [style.prop] | Style binding | [style.color]="color()" |
| @error / @fallback | Error boundary | @error="errorSig" |
Development
pnpm build # Build package
pnpm test # Run tests
pnpm test -- --watch # Watch modeTesting
| Feature | Unit | Integration | E2E | Status | | --- | --- | --- | --- | --- | | compileNodes | - | - | - | Missing unit, integration, e2e | | compileTemplate | - | - | - | Missing unit, integration, e2e | | bindTemplateDirectives | - | - | - | Missing unit, integration, e2e | | isSSRMode | - | - | - | Missing unit, integration, e2e | | toTextValue | - | - | - | Missing unit, integration, e2e | | __applyBindings | - | - | - | Missing unit, integration, e2e | | bootstrap | ✓ | - | - | Missing integration, e2e | | createContextProxy | - | - | - | Missing unit, integration, e2e | | loadWithTimeout | - | - | - | Missing unit, integration, e2e | | scheduleByPriority | - | - | - | Missing unit, integration, e2e | | html | - | - | - | Missing unit, integration, e2e | | onDestroy | ✓ | - | - | Missing integration, e2e | | bindBoundAttributes | - | - | - | Missing unit, integration, e2e | | bindClassDirectives | - | - | - | Missing unit, integration, e2e | | bindComponentTags | - | - | - | Missing unit, integration, e2e | | bindDefaultTwoWay | - | - | - | Missing unit, integration, e2e | | bindEventHandlers | - | - | - | Missing unit, integration, e2e | | bindTextInterpolations | - | - | - | Missing unit, integration, e2e | | buildPidMap | - | - | - | Missing unit, integration, e2e | | buildCommentMap | - | - | - | Missing unit, integration, e2e | | nearestCtx | - | - | - | Missing unit, integration, e2e | | mergeContexts | - | - | - | Missing unit, integration, e2e | | compileWithEvent | - | - | - | Missing unit, integration, e2e |
| Feature | Unit | Integration | E2E | Status | | --- | --- | --- | --- | --- | | bindSwitchCases | - | - | - | Missing unit, integration | | handleConditionalChain | - | - | - | Missing unit, integration | | bindConditionals | - | - | - | Missing unit, integration | | bindForLoops | - | - | - | Missing unit, integration | | createLoopManager | - | - | - | Missing unit, integration | | safeInsertBefore | - | - | - | Missing unit, integration | | markElementsAsLoopGenerated | - | - | - | Missing unit, integration | | createItemRange | - | - | - | Missing unit, integration | | wrapInRange | - | - | - | Missing unit, integration | | detachRange | - | - | - | Missing unit, integration | | createLoopSignals | - | - | - | Missing unit, integration | | updateLoopSignals | - | - | - | Missing unit, integration | | createLoopContext | - | - | - | Missing unit, integration | | findAllNestedLoopNodes | - | - | - | Missing unit, integration |
| Feature | Unit | Integration | E2E | Status | | --- | --- | --- | --- | --- | | LazyState | - | - | - | Missing unit, integration, e2e | | LazyOptions | - | - | - | Missing unit, integration, e2e | | LazyHandle | ✓ | - | - | Missing integration, e2e | | lazy | ✓ | - | - | Missing integration, e2e | | isLazyHandle | ✓ | - | - | Missing integration, e2e | | preloadLazy | ✓ | - | - | Missing integration, e2e | | ModuleRecord | ✓ | - | - | Missing integration, e2e | | loadModule | ✓ | - | - | Missing integration, e2e | | getModuleRecord | ✓ | - | - | Missing integration, e2e | | invalidateModule | ✓ | - | - | Missing integration, e2e | | getResolvedModuleKeys | ✓ | - | - | Missing integration, e2e | | clearModuleCache | ✓ | - | - | Missing integration, e2e | | bindDeferDirectives | ✓ | - | - | Missing integration | | registerDirectiveBinder | ✓ | - | - | Missing integration, e2e | | unregisterDirectiveBinder | ✓ | - | - | Missing integration, e2e | | executeCustomDirectiveBinders | ✓ | - | - | Missing integration, e2e |
| Feature | Unit | Integration | E2E | Status | | --- | --- | --- | --- | --- | | FocusTrap | ✓ | - | - | Missing integration | | activate | ✓ | - | - | Missing integration | | deactivate | ✓ | - | - | Missing integration | | updateFocusableElements | ✓ | - | - | Missing integration | | isActivated | ✓ | - | - | Missing integration | | destroy | ✓ | - | - | Missing integration | | createFocusTrap | ✓ | - | - | Missing integration | | isKey | ✓ | - | - | Missing integration | | isActivationKey | ✓ | - | - | Missing integration | | onEscape | ✓ | - | - | Missing integration | | onEnter | ✓ | - | - | Missing integration | | onSpace | ✓ | - | - | Missing integration | | onActivation | ✓ | - | - | Missing integration | | onArrowKeys | ✓ | - | - | Missing integration | | hasModifierKeys | ✓ | - | - | Missing integration | | onKeyboardShortcut | ✓ | - | - | Missing integration | | makeKeyboardAccessible | ✓ | - | - | Missing integration | | RovingTabindex | ✓ | - | - | Missing integration | | focusItem | ✓ | - | - | Missing integration | | updateItems | ✓ | - | - | Missing integration | | getCurrentIndex | ✓ | - | - | Missing integration | | getCurrentItem | ✓ | - | - | Missing integration | | destroy | ✓ | - | - | Missing integration | | createRovingTabindex | ✓ | - | - | Missing integration | | SkipLink | ✓ | - | - | Missing integration | | SkipLinks | ✓ | - | - | Missing integration |
Related Packages
- @pithyjs/signals - Standalone reactive signal system
- @pithyjs/router - File-based client-side router
- @pithyjs/compiler - .pithy file parser
- @pithyjs/vite-plugin - Vite integration
Documentation
- Runtime System - Lazy loading, module caching, and runtime utilities
- Compilation System - Template compilation and directive binding
- For Loops - @for directive documentation
- Project Overview - Vision, philosophy, and architecture
- Development Guide - Setup, workflows, and best practices
License
MIT
