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 🙏

© 2025 – Pkg Stats / Ryan Hefner

tinypine

v1.5.0

Published

Minimal, comfortable & intuitive reactive micro-framework with smart routing

Readme

TinyPine.js

Minimal reactive micro-framework

Zero build · Zero config · Just write HTML

Version Size Tests License


📋 Table of Contents


⚡ Quick Start

CDN (Easiest)

<!DOCTYPE html>
<html>
    <head>
        <title>My App</title>
    </head>
    <body>
        <div t-data="{ message: 'Hello TinyPine!' }">
            <h1 t-text="message"></h1>
            <button t-click="message = 'Clicked!'">Click me</button>
        </div>

        <script src="https://unpkg.com/[email protected]/dist/tinypine.min.js"></script>
        <script>
            TinyPine.init();
        </script>
    </body>
</html>

NPM

npm install tinypine
import TinyPine from "tinypine";
TinyPine.init();

🚀 CLI Tool (v1.1.0)

Create TinyPine projects in seconds:

# Create a new project
npx tinypine-cli new myapp

# Start development server
cd myapp && npx tinypine-cli serve

# Add features (router, i18n, ui)
npx tinypine-cli add router
npx tinypine-cli add i18n
npx tinypine-cli add ui

# Build for production
npx tinypine-cli build

Features:

  • 🎨 Templates: Vanilla, Tailwind, SPA, SSR, UI Ready
  • 🔌 Modular add-ons: router, i18n, ui, devtools
  • ⚡ Vite dev/build integration

Learn more →


✨ What's New

🧭 Routes & Guards (v1.5.0)

Smart client-side routing with navigation guards, dynamic params, and programmatic control:

Smart Router with Dynamic Parameters:

<nav>
    <a t-link="'home'">Home</a>
    <a t-link="'about'">About</a>
    <a t-link="'user/42'">User Profile</a>
</nav>

<!-- Route Views -->
<div t-route="'home'">
    <h1>Welcome Home!</h1>
</div>

<div t-route="'about'">
    <h1>About Us</h1>
</div>

<!-- Dynamic Parameters -->
<div t-route="'user/:id'">
    <div t-data="{}">
        <h1 t-text="'User ID: ' + $route.params.id"></h1>
        <p t-text="'Path: ' + $route.path"></p>
    </div>
</div>

<!-- 404 Fallback -->
<div t-route="*">
    <h1>404 - Page Not Found</h1>
</div>

<script>
    TinyPine.router({
        default: "home",
    });
    TinyPine.init();
</script>

Navigation Guards:

TinyPine.router({
    // Global beforeEnter guard
    beforeEnter(to, from) {
        const isLoggedIn = TinyPine.store("auth").loggedIn;

        if (!isLoggedIn && to.path !== "login") {
            return "/login"; // Redirect to login
        }

        return true; // Allow navigation
    },

    // Global beforeLeave guard
    beforeLeave(to, from) {
        if (hasUnsavedChanges) {
            return confirm("Discard unsaved changes?");
        }
        return true;
    },

    // Route-specific guards
    routes: {
        admin: {
            beforeEnter: async (to, from) => {
                const isAdmin = await checkAdminRole();
                return isAdmin ? true : "/";
            },
        },
    },
});

Programmatic Navigation:

<div t-data="{ userId: 42 }">
    <!-- Using $router in templates -->
    <button t-click="$router.push('/user/' + userId)">View Profile</button>

    <button t-click="$router.back()">Go Back</button>

    <!-- Access route info -->
    <p t-text="'Current: ' + $router.current().path"></p>
</div>

t-link Directive with Active State:

<nav>
    <a t-link="'home'" class="nav-link">Home</a>
    <a t-link="'about'" class="nav-link">About</a>
    <a t-link="'contact'" class="nav-link">Contact</a>
</nav>

<style>
    .nav-link.active {
        color: blue;
        font-weight: bold;
    }
</style>

Route Lifecycle Events:

// Listen to route changes
TinyPine.router.on("route:change", (to, from) => {
    console.log("Navigated from", from.path, "to", to.path);

    // Track page views
    if (typeof gtag !== "undefined") {
        gtag("config", "GA_ID", { page_path: to.path });
    }
});

// Other events: route:enter, route:leave, route:error
TinyPine.router.on("route:enter", (to) => {
    document.title = `My App - ${to.path}`;
});

Router Features:

  • ✅ Dynamic route matching (user/:id, blog/:category/:post)
  • ✅ Nested routes support
  • ✅ Wildcard fallback (* for 404s)
  • ✅ Navigation guards (sync & async)
  • ✅ Query parameters (?tab=posts&page=1)
  • ✅ Programmatic navigation API
  • ✅ Active link detection
  • ✅ Route lifecycle events
  • ✅ Scroll behavior control

📖 Complete Router Guide →


�� Async Flow & Forms (v1.4.0)

Powerful async operations and comprehensive form validation:

Enhanced t-fetch with Debounce:

<div t-data="{ search: '', results: [] }">
    <!-- Debounce fetch requests -->
    <button
        t-fetch="'/api/search?q=' + search"
        debounce="500"
        method="POST"
        headers="{ 'Authorization': 'Bearer ' + token }"
    >
        Search
    </button>

    <!-- Lifecycle hooks -->
    <script>
        TinyPine.context({
            beforeFetch: async ({ url, method }) => {
                console.log("Starting request:", url);
                return true; // or false to cancel
            },
            afterFetch: ({ data }) => {
                console.log("Received:", data);
            },
        });
    </script>
</div>

Form Validation System:

<tp-form>
    <div t-data="{ email: '', password: '' }">
        <!-- Built-in validators -->
        <input t-model="email" t-validate="required|email" name="email" />

        <input
            t-model="password"
            t-validate="required|minLength:8"
            name="password"
            type="password"
        />

        <!-- Show validation errors -->
        <p t-show="$errors.email" t-text="$errors.email[0]?.message"></p>

        <!-- Disable submit if invalid -->
        <button type="submit" :disabled="$invalid">Submit</button>
    </div>
</tp-form>

Debounced Inputs:

<div t-data="{ searchQuery: '' }">
    <!-- Update state after 300ms of inactivity -->
    <input t-model="searchQuery" t-debounce="300" />
    <p t-text="'Searching for: ' + searchQuery"></p>
</div>

Built-in Validators:

  • required - Field must not be empty
  • email - Valid email format
  • min:N / max:N - Numeric range
  • minLength:N / maxLength:N - String length
  • pattern:regex - Custom regex pattern
  • url - Valid URL format
  • number - Numeric value
  • integer - Whole number

Form State Variables:

  • $errors - Validation error messages per field
  • $valid / $invalid - Overall form validity
  • $touched - Fields that have been focused
  • $dirty - Fields that have been modified
  • $pending - Async operations in progress

�🔒 Core Stability (v1.3.0)

Enhanced security and developer experience with powerful new features:

Safe Expression Evaluator:

<!-- Multiple statements with semicolons -->
<div t-data="{ count: 0 }">
    <button t-click="const doubled = count * 2; count = doubled">Double</button>
</div>

<!-- Ternary operators -->
<p t-text="count > 10 ? 'High' : 'Low'"></p>

<!-- Arrow functions -->
<button t-click="items.filter(x => x.active).length">Active Count</button>

XSS-Protected HTML Rendering:

<div t-data="{ content: '<p>Safe HTML</p>' }">
    <!-- Automatically sanitizes dangerous tags and scripts -->
    <div t-html="content"></div>
</div>

Shorthand Binding Syntax:

<!-- Use :attr instead of t-bind:attr -->
<img :src="imageUrl" :alt="imageAlt" />
<div :class="activeClass" :id="elementId"></div>

Enhanced Loop Context:

<ul>
    <li t-for="item in items" t-class:first="$first" t-class:last="$last">
        <span t-text="$index + 1"></span>: <span t-text="item"></span>
    </li>
</ul>

New Form Components:

  • tp-field - Form field wrapper with labels, validation, and error states
  • tp-input - Text input with icons, sizes, and validation states
  • tp-checkbox - Custom checkbox with labels
  • tp-file-upload - Drag & drop file upload with preview

📖 Form Components Guide →


🎨 TinyPine UI (v1.2.0)

Ready-to-use Tailwind CSS components:

<script src="https://unpkg.com/[email protected]/dist/tinypine.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/tinypine.ui.min.js"></script>
<link
    rel="stylesheet"
    href="https://unpkg.com/[email protected]/dist/tinypine.ui.css"
/>
<script src="https://cdn.tailwindcss.com"></script>

<div t-data="{ open: false }">
    <tp-button color="primary" size="md" icon="check">Save</tp-button>
    <tp-modal t-show="open" title="Confirm">
        <p>Are you sure?</p>
        <tp-button color="outline" t-click="open = false">Cancel</tp-button>
    </tp-modal>
    <tp-card title="User Info">
        <p>Content goes here</p>
    </tp-card>
</div>

<script>
    TinyPine.init();
</script>

Available Components:

  • tp-button - Button with color, size, type, icon props
  • tp-modal - Modal with title prop, auto backdrop/close
  • tp-card - Card with title prop, Tailwind styling

Theme Support:

TinyPine.theme = "dark"; // or 'light'

📖 Complete UI Usage Guide → | 📖 Türkçe Kılavuz →


🔄 Component Lifecycle (v1.1.2+)

Mount Lifecycle

Run code after a component is rendered:

<div
    t-data="{
  count: 0,
  mounted(el, ctx) {
    el.classList.add('mounted');
    console.log('Component mounted!');
  }
}"
>
    <span t-text="'Count: ' + count"></span>
    <button t-click="count++">+1</button>
</div>

Global mount listener:

TinyPine.onMount((el, ctx) => {
    console.log("🌱 Mounted:", el);
});

Unmount Lifecycle (v1.1.3)

Run cleanup when a component is removed:

<div
    t-data="{
  count: 0,
  beforeUnmount(el, ctx) {
    console.log('About to remove...');
  },
  unmounted(el, ctx) {
    console.log('Removed!');
  }
}"
>
    <span t-text="'Count: ' + count"></span>
</div>

Global unmount listener:

TinyPine.onUnmount((el, ctx) => {
    console.log("🧹 Cleaned up:", el);
});

Emits component:mounted and component:unmounted events via the global event bus.


🌱 Sprout.js Readiness (v1.1.1)

TinyPine v1.1.1 introduces features for educational and sandbox environments:

Lite Mode:

TinyPine.start("#app", { mode: "lite" });
// Disables: devtools, store, router, i18n

Safe Mode:

TinyPine.start("#app", { safe: true });
// Wraps all directive executions in try/catch

Silent Debug:

TinyPine.debugOptions.silent = true;
// Suppresses [TinyPine] console logs

Global Event Bus:

TinyPine.on("directive:click", (el, ctx) => {
    console.log("Click detected");
});

🎯 Core Features

  • 100 Passing Tests - Comprehensive test coverage
  • 📘 TypeScript Support - Full type definitions included
  • 🛠️ DevTools Integration - Live debugging & inspection
  • 🌍 i18n Ready - Built-in internationalization
  • 🔄 Global Store - Shared state management
  • 📡 Async Support - t-fetch, t-await directives
  • 🌐 Router System - Hash-based navigation
  • 🎨 Transitions - Smooth animations built-in
  • 🔌 Plugin API - Extensible architecture
  • 📊 Performance - Optimized for production

📚 Directives

Core Directives

| Directive | Description | Example | | --------- | -------------------------------------- | -------------------------------------- | | t-data | Create reactive scope | <div t-data="{ count: 0 }"> | | t-text | Update text content | <span t-text="message"></span> | | t-html | Update HTML content (XSS-safe, v1.3.0) | <div t-html="content"></div> | | t-show | Toggle visibility (with transitions) | <p t-show="isVisible">Hello</p> | | t-click | Click handlers | <button t-click="count++">+</button> | | t-model | Two-way binding | <input t-model="name"> |

List Rendering & Events

| Directive | Description | Example | | ---------------- | --------------- | ---------------------------------------- | | t-for | List rendering | <li t-for="item in items"> | | .prevent/.stop | Event modifiers | <button t-click="save.prevent"> | | t-init | Lifecycle hook | <div t-init="console.log('Mounted!')"> |

Advanced Bindings

| Directive | Description | Example | | -------------- | ------------------- | --------------------------------- | | t-bind | Dynamic attributes | <img t-bind:src="url"> | | t-class | Conditional classes | <div t-class:active="isActive"> | | t-ref | DOM references | <input t-ref="input"> | | t-transition | CSS transitions | <div t-transition="fade"> |


🎯 Examples

Counter App

<div t-data="{ count: 0 }">
    <button t-click="count--">-</button>
    <span t-text="count"></span>
    <button t-click="count++">+</button>
</div>

Todo List

<div
    t-data="{
  todos: [],
  newTodo: '',
  methods: {
    add() {
      if(this.newTodo) this.todos.push(this.newTodo);
      this.newTodo = '';
    },
    remove(i) {
      this.todos.splice(i, 1);
    }
  }
}"
>
    <input t-model="newTodo" placeholder="Add todo" />
    <button t-click="methods.add()">Add</button>

    <ul>
        <li t-for="(todo, i) in todos">
            <span t-text="todo"></span>
            <button t-click="methods.remove(i)">Remove</button>
        </li>
    </ul>
</div>

Login Form

<div
    t-data="{
  email: '',
  password: '',
  loggedIn: false,
  methods: {
    login() {
      this.loggedIn = true;
    }
  }
}"
>
    <div t-show="!loggedIn">
        <input t-model="email" placeholder="Email" />
        <input t-model="password" type="password" placeholder="Password" />
        <button t-click="methods.login()">Login</button>
    </div>

    <div t-show="loggedIn">
        <p t-text="'Welcome, ' + email"></p>
    </div>
</div>

🔧 Advanced Features

Global Store

Create shared state across components:

// Create stores
TinyPine.store("auth", { user: "Guest", loggedIn: false });
TinyPine.store("ui", { theme: "light" });

// Use in any component
<div t-data="{}">
    <span t-text="$store.auth.user"></span>
    <button t-click="$store.auth.loggedIn = true">Login</button>
</div>;

Custom Plugins

TinyPine.use({
    name: "Toast",
    init(TinyPine) {
        TinyPine.directive("toast", (el, message) => {
            alert(message);
        });
    },
});

Async Data Fetching

Enhanced t-fetch (v1.3.0)

Fetch data from APIs with race condition control, loading/error states, and lifecycle hooks:

<!-- Basic fetch with loading/error states -->
<div t-data="{ posts: [], $loading: false, $error: null }">
    <div t-fetch="'/api/posts'">
        <!-- Loading indicator -->
        <p t-show="$loading">⏳ Loading posts...</p>

        <!-- Error message -->
        <p t-show="$error" t-text="'Error: ' + $error"></p>

        <!-- Posts list -->
        <ul t-show="!$loading && !$error">
            <li t-for="post in posts">
                <h3 t-text="post.title"></h3>
            </li>
        </ul>
    </div>
</div>

<!-- Advanced: Lifecycle hooks -->
<div t-data="{ users: [] }">
    <div
        t-fetch="'/api/users'"
        @t:onFetchStart="console.log('Fetching...')"
        @t:onFetchEnd="console.log('Done!', $event.detail.data)"
        @t:onFetchError="console.error('Failed:', $event.detail.error)"
    >
        <ul>
            <li t-for="user in users" t-text="user.name"></li>
        </ul>
    </div>
</div>

<!-- Dynamic URL with reactive state -->
<div t-data="{ userId: 1, user: null }">
    <input t-model="userId" type="number" />
    <div t-fetch="'/api/users/' + userId">
        <p t-show="$loading">Loading...</p>
        <p t-text="user?.name"></p>
    </div>
</div>

Features:

  • Race Condition Control - Automatically cancels outdated requests
  • AbortController - Properly cancels requests when URL changes
  • State Variables - $loading, $error, $response automatically managed
  • Lifecycle Events - t:onFetchStart, t:onFetchEnd, t:onFetchError
  • Request Tracking - Prevents duplicate requests for same URL

t-await for Promises

Handle promises with loading and error states:

<div t-data="{ user: {} }">
    <div t-await="fetch('/api/user').then(r=>r.json())">
        <div t-loading="'Loading user...'">⏳ Loading...</div>
        <div t-error="'Failed to load user.'">❌ Error!</div>
        <p t-text="user?.name || ''"></p>
    </div>
</div>

Hash Router

Build SPAs with hash-based routing:

<nav>
    <a href="#/home">Home</a>
    <a href="#/about">About</a>
</nav>

<div t-route="'home'">🏠 Home Page</div>
<div t-route="'about'">ℹ️ About Page</div>

<script>
    TinyPine.router({
        default: "home",
        onChange(route) {
            console.log("Route changed →", route);
        },
    });
</script>

Internationalization (i18n)

Add multi-language support:

<script>
    // Setup translations
    TinyPine.i18n(
        {
            en: { greeting: "Hello World!", welcome: "Welcome!" },
            tr: { greeting: "Merhaba Dünya!", welcome: "Hoş geldiniz!" },
        },
        { default: "en", cache: true }
    );
</script>

<div t-data>
    <h1 t-text.lang="'greeting'"></h1>
    <button t-click="$lang = 'tr'">🇹🇷 Türkçe</button>
    <button t-click="$lang = 'en'">🇺🇸 English</button>
</div>

Dynamic Locale Loading:

TinyPine.loadLocale("tr", "/lang/tr.json");
TinyPine.loadLocale("en", "/lang/en.json");

Event Modifiers

<button t-click="save.prevent">Prevent default</button>
<button t-click="methods.init.once">Run once</button>
<button t-click="methods.close.outside">Close on outside click</button>

📦 Installation

CDN

<script src="https://unpkg.com/[email protected]/dist/tinypine.min.js"></script>
<script>
    TinyPine.init();
</script>

NPM

npm install tinypine

TypeScript Support:

import TinyPine from "tinypine";
// Full IntelliSense and type checking

Testing

npm test

Test Coverage:

  • ✅ 100 passing tests (8 test files)
  • ✅ Core directives (t-text, t-show, t-click, t-model, t-for)
  • ✅ Form components (tp-input, tp-checkbox, tp-file-upload)
  • ✅ Global stores and reactivity
  • ✅ Keyed list diffing (v1.3.1)
  • ✅ Router and validation (v1.3.1)
  • ✅ Context management and lifecycle hooks
  • ✅ Edge cases and performance

📖 Testing Guide →


⚙️ API Reference

Core Methods

  • TinyPine.init(root?) - Initialize TinyPine
  • TinyPine.start(selector, opts?) - Start with options (lite/safe mode)
  • TinyPine.store(name, data) - Create global store
  • TinyPine.watch(path, callback) - Watch changes
  • TinyPine.use(plugin) - Register plugin
  • TinyPine.directive(name, handler) - Custom directive
  • TinyPine.component(name, config) - Register custom component
  • TinyPine.transition(name, config) - Register transition

Lifecycle & Events

  • TinyPine.onMount(callback) - Global mount listener
  • TinyPine.onUnmount(callback) - Global unmount listener
  • TinyPine.on(event, callback) - Event listener
  • TinyPine.off(event, callback) - Remove event listener
  • TinyPine.emit(event, ...args) - Emit event

Context Variables

  • $parent - Parent scope data
  • $root - Root scope data
  • $refs - DOM references
  • $el - Current element
  • $store - Global stores
  • $lang - Current i18n language

🎨 Styling

TinyPine works with any CSS framework:

<link
    href="https://cdn.jsdelivr.net/npm/tailwindcss@2/dist/tailwind.min.css"
    rel="stylesheet"
/>
<div t-data="{ count: 0 }" class="p-8">
    <button t-click="count++" class="px-4 py-2 bg-blue-500 text-white rounded">
        Count: <span t-text="count"></span>
    </button>
</div>

🐛 Debugging

Enable debug mode:

TinyPine.debug = true;
// Console will show: [TinyPine] count changed → 1

// Silent mode (suppress logs)
TinyPine.debugOptions.silent = true;

DevTools

Built-in developer tools panel:

<script>
    TinyPine.debug = true;
    TinyPine.devtools({ position: "bottom-right", theme: "dark" });
</script>

Features:

  • 📊 Live Store Inspector
  • 🎯 Context Viewer
  • ⏱️ Reactivity Timeline
  • 📈 Performance Monitor

🌍 Browser Support

✅ Chrome 49+ ✅ Firefox 18+ ✅ Safari 10+ ✅ Edge 49+

Works everywhere JavaScript Proxies are supported.


📈 Roadmap

  • v1.0.0 - Stable Release with TypeScript & Tests
  • v1.1.0 - CLI Tool & Ecosystem Expansion
  • v1.1.1 - Sprout.js Readiness (Lite/Safe Modes)
  • v1.1.2 - Component Mount Lifecycle
  • v1.1.3 - Component Unmount Lifecycle
  • v1.2.0 - TinyPine UI Components
  • v1.3.0 - Core Stability & Form Components
  • v1.3.1 - Keyed Diffing & Advanced Features (100% Test Coverage!)
  • v1.4.0 - Async Flow & Form Validation
  • v1.5.0 - Smart Router with Guards & Navigation
  • 🔜 v1.6.0 - Performance Optimizations & Virtual DOM (Planned)
  • 🔜 v1.7.0 - DevTools 2.0 & Router Inspector (Planned)
  • 🔜 v2.0.0 - Ecosystem & TypeScript-first Rewrite (Planned)

📚 Documentation

Comprehensive guides and best practices:


🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


📄 License

MIT License - Use freely in your projects!


GitHub · Issues · Documentation