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

adowire

v0.1.2

Published

A reactive component system for AdonisJS with Edge.js

Readme

Adowire

⚠️ Alpha Software — Adowire is in active development (0.x.x). APIs may change between minor versions. Not recommended for production use yet.

A full-stack reactive component system for AdonisJS v7 and Edge.js v6 — inspired by Laravel Livewire. Build dynamic, interactive UIs using server-side TypeScript classes and Edge templates. No frontend framework required.

What It Does

You write a TypeScript class with public properties and methods, pair it with an Edge template, and Adowire handles everything else — state serialization, AJAX roundtrips, DOM diffing, two-way binding, validation, streaming, and more.

// No React. No Vue. No manual fetch calls.
// Just TypeScript + Edge templates = reactive UI.

Table of Contents


Requirements

| Dependency | Version | | --- | --- | | Node.js | >= 24.0.0 | | AdonisJS | ^7.0.0 | | Edge.js | ^6.0.0 | | @adonisjs/assembler (optional, for CLI commands) | ^8.0.0 |


Installation

Install the package from npm:

npm install adowire

Then configure it using the Ace CLI:

node ace configure adowire

This will:

  1. Publish config/adowire.ts to your project
  2. Register the provider (adowire/wire_provider) and commands (adowire/commands) in adonisrc.ts
  3. Create the scaffold directories: app/adowire/ and resources/views/adowire/

Add Tags to Your Layout

In your main layout template (e.g. resources/views/layouts/main.edge), add the Adowire style and script tags:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ $title ?? 'My App' }}</title>
  @adowireStyles
</head>
<body>
  {{{ await $slots.main() }}}
  @adowireScripts
</body>
</html>
  • @adowireStyles — injects the [adowire:cloak] CSS rule to prevent FOUC
  • @adowireScripts — injects the client-side JavaScript bundle and CSRF meta tag

Quick Start

1. Create a Component

node ace make:adowire counter

This generates two files:

  • app/adowire/counter.ts — the component class
  • resources/views/adowire/counter.edge — the template

2. Write the Component Class

import { WireComponent } from 'adowire'

export default class Counter extends WireComponent {
  count = 0

  increment() {
    this.count++
  }

  decrement() {
    this.count--
  }

  reset() {
    this.count = 0
  }
}

3. Write the Template

<div>
  <h2>Count: {{ count }}</h2>

  <button adowire:click="increment">+</button>
  <button adowire:click="decrement">−</button>
  <button adowire:click="reset">Reset</button>
</div>

4. Use It in Any Page

@layout('layouts/main')

@section('content')
  @adowire('counter')
  @end
@endsection

Or use the HTML-style syntax:

<adowire:counter />

Components

A component is a TypeScript class that extends WireComponent. Public properties become reactive state. Public methods become callable actions.

import { WireComponent } from 'adowire'

export default class TodoList extends WireComponent {
  // Reactive state — automatically synced with the client
  items: string[] = []
  newItem = ''

  // Actions — callable from the template
  add() {
    if (this.newItem.trim()) {
      this.items.push(this.newItem.trim())
      this.newItem = ''
    }
  }

  remove(index: number) {
    this.items.splice(index, 1)
  }
}

State Helpers

Every component has built-in methods for managing state:

| Method | Description | | --- | --- | | fill(data) | Bulk-assign properties from an object | | reset(...props) | Reset properties to their initial values (all if no args) | | pull(...props) | Reset and return the old values | | only(...props) | Return a subset of public state | | all() | Return all public state as a plain object |

Magic Actions

These methods trigger special effects on the client:

| Method | Description | | --- | --- | | $refresh() | Re-render the component without calling any action | | $set(prop, value) | Set a public property from the server | | $toggle(prop) | Toggle a boolean property | | $redirect(url) | Redirect the browser after the response | | $dispatch(event, params) | Dispatch a browser event | | $stream(name, content, replace?) | Stream content to a adowire:stream target | | $download(name, url) | Trigger a file download | | js(expression) | Execute a JavaScript expression on the client | | skipRender() | Skip re-rendering for the current request |


Rendering Components

Edge Tag Syntax

{{-- Basic --}}
@adowire('counter')
@end

{{-- With props --}}
@adowire('counter', { initialCount: 10 })
@end

HTML-Style Syntax

The HTML preprocessor transforms <adowire:...> tags at compile time:

{{-- Self-closing --}}
<adowire:counter />

{{-- With props --}}
<adowire:counter title="My Counter" />

{{-- Dynamic props (prefixed with :) --}}
<adowire:counter :count="someVariable" />

{{-- Boolean props --}}
<adowire:counter disabled />

{{-- Nested components (dot notation) --}}
<adowire:posts.create />

{{-- Kebab-case auto-converts to camelCase --}}
<adowire:counter initial-count="5" />

{{-- Dynamic component name --}}
<adowire:dynamic-component :is="activeTab" />

Page Components & Routing

Page components are full-page Adowire components that replace traditional controller + view pairs. Use the router.adowire() macro in your routes file:

import router from '@adonisjs/core/services/router'

// Simple page
router.adowire('/dashboard', 'dashboard')

// With route params — passed to mount()
router.adowire('/posts/:id', 'posts.show')

Scaffold a page component with the --page flag:

node ace make:adowire dashboard --page

This generates a component with @Layout and @Title decorators:

import { WireComponent, Layout, Title } from 'adowire'

@Layout('layouts/adowire')
@Title('Dashboard')
export default class Dashboard extends WireComponent {
  // Route params are passed to mount()
  mount(params: Record<string, any>) {
    // ...
  }
}

Lifecycle Hooks

| Hook | When It Runs | Request Type | | --- | --- | --- | | mount(props) | First request only (component initialization) | Initial | | boot() | Every request, before hydration | All | | hydrate() | After boot, on subsequent requests only | AJAX | | rendering(view, data) | Before Edge renders — can mutate data | All | | rendered(view, html) | After Edge renders — can mutate HTML string | All | | dehydrate() | End of every request, before snapshot | All | | updating(name, value) | Before a property is set from the client | AJAX | | updated(name, value) | After a property is set from the client | AJAX | | exception(error, stop) | On unhandled exception | All |

Property-Specific Hooks

You can define hooks that target a specific property by name:

// Called when 'search' is about to change
updatingSearch(value: string) {
  console.log('Search changing to:', value)
}

// Called after 'search' has changed
updatedSearch(value: string) {
  console.log('Search changed to:', value)
}

Decorators

@Computed()

Memoized per-request computed property. Accessible in templates like a regular property.

import { WireComponent, Computed } from 'adowire'

export default class Cart extends WireComponent {
  items: { price: number }[] = []

  @Computed()
  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0)
  }
}
<p>Total: ${{ total }}</p>

@Locked()

Prevents a property from being modified by the client. Throws LockedPropertyException if tampered with.

import { WireComponent, Locked } from 'adowire'

export default class Payment extends WireComponent {
  @Locked()
  price = 99.99  // Client cannot change this
}

@Validate(rule, options?)

Attaches a VineJS validation rule to a property. Validated automatically on update or manually via this.validate().

import { WireComponent, Validate } from 'adowire'
import vine from '@vinejs/vine'

export default class Registration extends WireComponent {
  @Validate(vine.string().email(), { message: 'Please enter a valid email' })
  email = ''

  @Validate(vine.string().minLength(8))
  password = ''
}

@Title(text)

Sets the browser's <title> for page components.

@Title('Settings')
export default class Settings extends WireComponent { /* ... */ }

@Layout(name, options?)

Wraps a page component in a layout template.

@Layout('layouts/adowire', { slot: 'main' })
export default class Dashboard extends WireComponent { /* ... */ }

Client-Side Directives

Directives are HTML attributes that wire up client-side behavior. All prefixed with adowire:.

adowire:click

Call a server-side method when the element is clicked.

<button adowire:click="increment">+1</button>

{{-- With arguments --}}
<button adowire:click="remove({{ id }})">Delete</button>

adowire:model

Two-way data binding between an input and a component property.

{{-- Deferred (synced on form submit) --}}
<input adowire:model="name" />

{{-- Live (syncs on every keystroke) --}}
<input adowire:model.live="search" />

{{-- Blur (syncs when the input loses focus) --}}
<input adowire:model.live.blur="email" />

{{-- Debounced (waits 500ms after the last keystroke) --}}
<input adowire:model.live.debounce.500ms="query" />

{{-- Throttled (at most once per second) --}}
<input adowire:model.live.throttle.1000ms="filter" />

adowire:submit

Intercept form submission and call a server-side method. Automatically collects adowire:model fields.

<form adowire:submit="save">
  <input adowire:model="title" />
  <textarea adowire:model="body"></textarea>
  <button type="submit">Save</button>
</form>

adowire:loading

Show, hide, or modify elements during server requests.

{{-- Show while loading (hidden at rest) --}}
<div adowire:loading>Loading...</div>

{{-- Hide while loading (visible at rest) --}}
<div adowire:loading.remove>Content loaded.</div>

{{-- Add a CSS class while loading --}}
<button adowire:loading.class="opacity-50">Submit</button>

{{-- Remove a CSS class while loading --}}
<div adowire:loading.class.remove="opacity-100">Content</div>

{{-- Set an attribute while loading --}}
<button adowire:loading.attr="disabled">Submit</button>

adowire:poll

Automatically refresh the component at an interval.

{{-- Poll every 2 seconds (default) --}}
<div adowire:poll>{{ timestamp }}</div>

{{-- Custom interval --}}
<div adowire:poll.5s>{{ notifications }}</div>

{{-- Only poll when the element is visible --}}
<div adowire:poll.10s.visible>{{ feed }}</div>

adowire:dirty

Show changes in the UI when local state differs from server state.

{{-- Show element when input is dirty --}}
<span adowire:dirty>Unsaved changes</span>

{{-- Add a class when dirty --}}
<input adowire:model.live="name" adowire:dirty.class="border-yellow-500" />

adowire:show

Toggle element visibility based on a JavaScript expression (no server roundtrip).

<div adowire:show="isOpen">Dropdown content</div>

adowire:cloak

Hide the element until the component has fully initialized. Prevents flash of unstyled content (FOUC).

<div adowire:cloak>This won't flash on load</div>

adowire:stream

Target element for real-time SSE streaming via $stream().

{{-- Append streamed content --}}
<div adowire:stream="response"></div>

{{-- Replace content instead of appending --}}
<div adowire:stream.replace="status"></div>

adowire:ignore

Exclude an element from DOM morphing. Useful for third-party widgets.

<div adowire:ignore>This content won't be touched by morphdom</div>

adowire:key

Provide a stable identity for morphdom diffing (like key in React/Vue).

@each(item in items)
  <div adowire:key="{{ item.id }}">{{ item.name }}</div>
@end

Validation

Adowire integrates with VineJS for server-side validation.

Using Decorators

import { WireComponent, Validate } from 'adowire'
import vine from '@vinejs/vine'

export default class ContactForm extends WireComponent {
  @Validate(vine.string().minLength(2))
  name = ''

  @Validate(vine.string().email())
  email = ''

  @Validate(vine.string().minLength(10))
  message = ''

  async submit() {
    await this.validate() // Throws on failure, populates $errors
    // If we get here, all fields are valid
  }
}

Using a Compiled Validator

import vine from '@vinejs/vine'

const validator = vine.compile(
  vine.object({
    name: vine.string(),
    email: vine.string().email(),
  })
)

export default class MyForm extends WireComponent {
  name = ''
  email = ''

  async submit() {
    const data = await this.validateUsing(validator)
    // data is fully typed
  }
}

Displaying Errors in Templates

<input adowire:model.live="email" />

@error('email')
  <span class="text-red-500">{{ message }}</span>
@enderror

Streaming

Send real-time content to the browser using Server-Sent Events:

export default class Chat extends WireComponent {
  prompt = ''

  async ask() {
    for await (const chunk of getAIResponse(this.prompt)) {
      this.$stream('answer', chunk)
    }
  }
}
<form adowire:submit="ask">
  <input adowire:model="prompt" />
  <button type="submit">Ask</button>
</form>
<div adowire:stream="answer"></div>

Alpine.js Integration

Adowire provides a $wire magic proxy for Alpine.js interop:

<div x-data>
  {{-- Read state --}}
  <span x-text="$wire.count"></span>

  {{-- Set state --}}
  <button @click="$wire.count = 0">Reset</button>

  {{-- Call actions --}}
  <button @click="await $wire.increment()">+1</button>
</div>

CLI Commands

Adowire ships with 7 Ace commands for scaffolding and managing components.

make:adowire

Scaffold a new component class and/or Edge template.

# Component class + view
node ace make:adowire counter

# Nested component
node ace make:adowire posts/create

# Page component (adds @Layout + @Title decorators)
node ace make:adowire dashboard --page

# Class only (no view)
node ace make:adowire counter --class

# View only (no class)
node ace make:adowire counter --view

| Flag | Short | Description | | --- | --- | --- | | --page | -p | Scaffold as a page component with @Layout and @Title | | --class | -c | Generate only the TypeScript class | | --view | -v | Generate only the Edge template |

adowire:list

List all registered components.

node ace adowire:list

# Output as JSON
node ace adowire:list --json

| Flag | Short | Description | | --- | --- | --- | | --json | -j | Output the component list as JSON |

adowire:layout

Create a default Adowire layout template.

node ace adowire:layout

# Custom name
node ace adowire:layout --name app

| Flag | Short | Description | | --- | --- | --- | | --name | -n | Layout file name (default: adowire) | | --force | -f | Overwrite if the file already exists |

adowire:move

Rename or move a component (both class and view), automatically updating the class name.

node ace adowire:move posts/create posts/entry

adowire:delete

Delete a component's class and view files.

node ace adowire:delete counter

# Skip confirmation prompt
node ace adowire:delete counter --force

| Flag | Short | Description | | --- | --- | --- | | --force | -f | Skip the confirmation prompt |

adowire:stubs

Publish Adowire's stub templates to your project for customization.

node ace adowire:stubs

Stubs are published to stubs/vendor/adowire/.

| Flag | Short | Description | | --- | --- | --- | | --force | -f | Overwrite existing stubs |

configure adowire

Initial setup command (run once after installation).

node ace configure adowire

Configuration

After running node ace configure adowire, you'll find config/adowire.ts in your project:

import { defineConfig } from 'adowire'

export default defineConfig({
  // URL prefix for the Adowire message endpoint
  prefix: '/adowire',

  // Directory where component classes live
  componentsPath: 'app/adowire',

  // Edge.js view prefix for component templates
  viewPrefix: 'adowire',

  // Default layout for page components
  defaultLayout: 'layouts/adowire',

  // Enable dev-mode template variable proxy (warns on undefined access)
  devProxy: true,
})

All Options

| Option | Default | Description | | --- | --- | --- | | prefix | '/adowire' | URL prefix for the message endpoint | | componentsPath | 'app/adowire' | Where component classes live | | viewPrefix | 'adowire' | Edge.js view prefix | | secret | process.env.APP_KEY | HMAC signing secret for snapshots | | namespaces | — | Named namespace → directory mappings | | defaultLayout | — | Fallback layout for router.adowire() pages | | devProxy | true in dev | Warns on undefined template variable access | | injectMorphMarkers | true | Morph markers around @if/@each blocks |


Security

Adowire includes multiple layers of protection:

| Threat | Mitigation | | --- | --- | | Snapshot tampering | HMAC-SHA256 checksum on every snapshot, verified with timingSafeEqual | | Locked property mutation | @Locked() throws LockedPropertyException before hydration | | Calling private methods | $isCallable() blocks $/_ prefixed methods, lifecycle hooks, and reserved names | | CSRF attacks | AdonisJS CSRF middleware; token sent via X-CSRF-TOKEN header | | Sensitive data leakage | Protected/private properties are excluded from the client snapshot |


What Works & What's Coming

✅ Implemented

  • Core reactive engine (snapshot, hydration, dehydration, DOM morphing)
  • Public properties & two-way model binding
  • Server-side actions callable from templates
  • Full lifecycle hook system
  • Real-time streaming ($stream + SSE)
  • 5 decorators (@Computed, @Locked, @Validate, @Title, @Layout)
  • 10+ client-side directives (click, model, submit, loading, poll, dirty, show, cloak, stream, ignore, key)
  • HTML-style component tags (<adowire:counter />)
  • Page components with router.adowire() macro
  • Alpine.js $wire bridge
  • 7 Ace CLI commands
  • VineJS validation integration
  • Dev-mode template safety proxy
  • HMAC snapshot security

🚧 Coming Soon

  • WireForm — dedicated form component with file uploads
  • Events & Nesting$dispatch, @On decorator, parent/child communication
  • Islands & Lazy Loading — partial hydration, @Lazy decorator
  • SPA Navigationadowire:navigate for seamless page transitions
  • File Uploads — direct upload support with progress tracking
  • Pagination — automatic cursor/offset pagination helpers
  • URL State@Url decorator for query string binding
  • Teleport — render component output into a different DOM location
  • Testing UtilitiesWireTest harness for Japa

Contributing

Contributions are welcome! Since the project is in alpha, please open an issue first to discuss what you'd like to change.

# Clone & install
git clone https://github.com/hackerrahul/adowire.git
cd adowire && npm install

# Build
npm run build

# Lint
npm run lint

License

MIT License © Rahul Gangotri