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

kancil-framework

v0.1.0

Published

Bun-native MVC framework untuk aplikasi web. **Zero npm framework dependencies** — tidak menggunakan Express, Hono, Fastify, atau framework JS lainnya. Dibangun di atas `Bun.serve({ routes })`, Handlebars views, dan Bun.SQL database.

Readme

kancil-framework

Bun-native MVC framework untuk aplikasi web. Zero npm framework dependencies — tidak menggunakan Express, Hono, Fastify, atau framework JS lainnya. Dibangun di atas Bun.serve({ routes }), Handlebars views, dan Bun.SQL database.

⚠️ Bun-only. Membutuhkan Bun 1.3.14+.

Instalasi

bun add kancil-framework handlebars

Fitur opsional (PDF, QR, Barcode):

bun add pdfkit qrcode bwip-js

Quick Start

project/
  server.js
  .env
  app/
    themes/default/
      layout.hbs
      nav.hbs
      footer.hbs
      index.hbs
    modules/
      page/
        routes.js
        controllers/PageController.js
  migrations/
  seeds/
  storage/logs/

server.js

import { Application, Config, DB, Cors, Migration, Seeder } from 'kancil-framework';

const config = new Config();
DB.init(config.get('DATABASE_URL'));

await Migration.run(import.meta.dir + '/migrations');
await Seeder.run(import.meta.dir + '/seeds');

const app = new Application({ basePath: import.meta.dir + '/app' });
app.use(Cors());
await app.loadModules(import.meta.dir + '/app/modules');
await app.listen();

.env

PORT=3000
DATABASE_URL=mysql://root:root@localhost:3306/myapp
APP_KEY=your-random-32-char-key-here
THEME=default
APP_ENV=development

Module app/modules/page/routes.js

import PageController from './controllers/PageController.js';

export default app => {
    app.get('/', PageController.home);
};

Controller app/modules/page/controllers/PageController.js

import { Reply, View } from 'kancil-framework';

export default class PageController {
    static home(req) {
        return Reply.html(View.render('index', { title: 'Home' }));
    }
}

Template app/themes/default/index.hbs

<h1>{{title}}</h1>

Project Structure

app/
  themes/{theme}/
    layout.hbs           ← layout utama ({{body}})
    {page}.hbs           ← halaman per view
    partials/            ← partial (nav.hbs, footer.hbs, pagination.hbs)
    _system/partials/    ← partial sistem (override kalo perlu)
  modules/{name}/
    module.js            ← { prefix: '/custom-path' } (opsional)
    routes.js            ← export default app => { ... }
    controllers/         ← class dengan static method handlers
    models/              ← extends Model { static table = '...' }
migrations/              ← { up: async db => { ... }, down: ... }
seeds/                   ← { up: async db => { ... }, down: ... }
storage/
  logs/                  ← {date}.log
  trash.json             ← auto-generated oleh Trash

Routing

app.get('/path', handler);
app.post('/path', csrf, handler);
app.put('/path', handler);
app.delete('/path', handler);

Middleware via parameter array:

app.get('/protected', authMiddleware, handler);

Prefix module otomatis berdasarkan nama folder, atau via module.js:

// app/modules/auth/module.js
export default { prefix: '/auth' };
// routes.js: app.get('/login', ...) → /auth/login

Core API

Application

| Method | Description | |--------|-------------| | get(path, ...handlers) | Route GET | | post(path, ...handlers) | Route POST | | use(middleware) | Global middleware | | tap(fn) | Response tap — transform reply sebelum dikirim | | loadModules(dir) | Auto-discover modules | | listen() | Start server |

Reply

| Method | Description | |--------|-------------| | Reply.html(str) | HTML response | | Reply.json(data) | JSON response | | Reply.redirect(url) | Redirect 302 | | Reply.text(str) | Plain text | | Reply.pdf(buffer, filename) | PDF download | | Reply.xlsx(data, filename) | XLSX download | | Reply.csv(data, filename) | CSV download | | .withFlash(key, val) | Flash message (session) |

View

View.render('template', { data })   // render + layout
View.renderPartial('name', { data }) // partial tanpa layout

Flash otomatis tersedia sebagai {{flash.success}} / {{flash.error}} di template.

Handlebars helpers: {{rupiah 100000}}Rp 1.000.000, {{#if_eq a b}}...{{/if_eq}}.

DataTable

Server-side DataTable dengan AJAX, search, sort, pagination, bulk actions.

import { DataTable, Reply, View } from 'kancil-framework';

const options = {
    columns: {
        nama: { label: 'Nama', sortable: true },
        harga: { label: 'Harga', sortable: true, render: (v) => 'Rp ' + v.toLocaleString('id-ID') }
    },
    actions: {
        edit: '/produk/{id}/edit',
        delete: '/produk/{id}/delete'
    },
    searchable: ['nama'],
    orderable: { nama: 'nama' },
    defaultOrder: 'id DESC',
    bulkActions: true,
    bulkDeleteUrl: '/produk/bulk-delete'
};

// Page handler
static async page(req) {
    return Reply.html(View.render('datatable', {
        ...await DataTable.make(req, Produk, options)
    }));
}

// AJAX data handler
static async data(req) {
    return Reply.json(await DataTable.make(req, Produk, options));
}

// Bulk delete — 1 baris
static async bulkDelete(req) {
    return DataTable.bulkDeleteReply(req, Model, '/produk');
}

Template:

<input type="text" id="search-input" onkeyup="DataTable.search()">
<div id="table-wrapper" data-dt-url="/produk/data">{{{tableHtml}}}</div>
<div id="pagination-wrapper">{{{paginationHtml}}}</div>

Trash

Otomatis mencatat soft-deleted record dari semua model ke storage/trash.json.

import { Trash } from 'kancil-framework';

Trash.list(search, page, perPage);             // { data, total, pages }
await Trash.restore(id);                        // restore data + hapus dari trash
await Trash.remove(id);                         // hapus dari trash tanpa restore
Trash.empty();                                   // kosongkan semua trash

Tidak perlu setup — setiap Model.remove() otomatis log ke trash untuk model yang menggunakan useSoftDelete().

Model

import { Model } from 'kancil-framework';

export default class Todo extends Model {
    static table = 'todos';
    static { this.useSoftDelete(); }  // enable soft delete

    static async list(page) {
        return this.paginateWhere(page, 10, {}, { orderBy: 'created_at DESC' });
    }
}

| Method | Description | |--------|-------------| | find(id) | Find by primary key | | findAll(opts) | All records | | findWhere(where, opts) | Filtered | | create(data) | Insert | | update(id, data) | Update | | remove(id) | Soft/hard delete (smart) + auto Trash log | | restore(id) | Restore soft-deleted | | paginate(sql, page, perPage) | Pagination | | paginateWhere(page, perPage, where, opts) | Pagination with filter | | search(q, page, perPage, fields) | Search with LIKE | | upsert(data) | Create or update |

Opsi: { withDeleted: true } untuk include deleted, { $deleted: 'only' } untuk hanya deleted.

CSRF

Stateless time-based token. Setiap form WAJIB:

<input type="hidden" name="_csrf" value="<!-- csrf-token -->">

Auto-injected oleh framework di response body. Middleware csrf:

import csrf from './middleware/csrf.js';
app.post('/save', csrf, handler);

Session

In-memory session, lazy-init.

req.state.session.get('key');
req.state.session.set('key', val);
req.state.session.setFlash('success', 'Berhasil!');
// di template: {{flash.success}}

Validator

import { Validator } from 'kancil-framework';
const v = await Validator.make(data, { title: 'required|min:3|max:255' }, {
    'title.min': 'Minimal 3 karakter'
});
v.pass → boolean
v.errors → { field: 'message' }
v.data → sanitized data

Rules: required, min:n, max:n, email, numeric.

Rate Limiter

import { RateLimiter } from 'kancil-framework';
app.use(RateLimiter.middleware(100, 60));        // 100 req/menit global
app.get('/ping', RateLimiter.middleware(5, 30)); // 5 req/30 detik per route

Cache

FIFO cache (2000 slot). Otomatis cache GET text/html 200 tanpa flash. Manual:

req.state.cache.set(key, data);
req.state.cache.get(key);
req.state.cache.clear();

Other Features

| Import | Description | |--------|-------------| | Storage | Storage.disk.put/get/delete, Storage.s3 | | Paginator | paginate(query, page, perPage) | | Cart | Cart(session), .add(), .items(), .total(), .clear() | | Api | Api.success(data), Api.error(msg, code) | | Http | Fetch wrapper: Http.base(url).get('/path') | | Event | Event.on(), Event.emit(), Event.off() | | Mailer | Mailer.send({ to, subject, text, html }) via sendmail | | Str | Str.ulid(), Str.uuid(), Str.slug(), Str.random() | | Crypt | AES-256-GCM encrypt/decrypt via APP_KEY | | Debug | dump(), dd(), bench(), benchAsync() | | Logger | Logger.info(), Logger.error() ke file + console | | Pdf | Pdf.make(doc => { doc.text(...) }) | | Qr | Qr.toDataURL(text), Qr.toBuffer(text) | | Barcode | Barcode.toBuffer(text, { format }) | | Migration | Migration.run(dir) — migrate up | | Seeder | Seeder.run(dir) — seed data | | FileValidator | FileValidator.validate(file, rules) — mime, size, image |

Environment Variables

| Variable | Default | Description | |----------|---------|-------------| | PORT | 3000 | Server port | | DATABASE_URL | sqlite://:memory: | MySQL: mysql://user:pass@host:3306/db | | APP_KEY | — | Wajib. Untuk CSRF & Crypt (min 32 char) | | THEME | default | Nama theme folder | | APP_ENV | development | production — disable cache busting | | CSRF_TIMEOUT | 60 | CSRF token expiry (detik) | | SESSION_TTL_HOURS | 24 | Session lifetime (jam) | | CACHE_MAX | 2000 | Max cache slots | | STORAGE_PATH | storage/app | Local storage base path |

Client-side JS

Framework menyediakan 2 file JS yang otomatis di-serve:

| File | Description | |------|-------------| | /kancil-ui.js | UI components — toast, dialog, confirmForm, loading. Auto-inject di <head> | | /datatable.js | DataTable client — load, sort, search, bulk delete. Auto-inject di <head> | | /kancil.css | Stylesheet untuk Kancil components. Auto-inject |

kancil-client.js juga tersedia (/kancil-client.js) — state management, AJAX helper, auth, cart, cache, validate, bind — load manual jika dibutuhkan.

License

MIT