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

@devchitchat/index97

v3.2.1

Published

A Bun-native web framework that trusts the platform. File-based routing, zero build steps, HTML that works, JavaScript that ships.

Readme

index97

A Bun-native web framework. File-based routing, server-side templates, zero config.


Prerequisites

Install Bun:

curl -fsSL https://bun.sh/install | bash

Phase 1 — Up and running in 5 minutes

1. Create a project

mkdir my-site && cd my-site
bun init -y
bun add @devchitchat/index97

2. Create the entry point

// server.js
import { createServer } from '@devchitchat/index97'
createServer({ pagesDir: './pages' })

3. Create your first page and layout

mkdir pages pages/public
<!-- pages/_layout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{slot:title || My Site}}</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
  <main>
    {{content}}
  </main>
</body>
</html>
<!-- pages/index.html -->
<template data-slot="title">Home — My Site</template>

<h1>Hello, world.</h1>
<include src="_greeting.phtml">
<!-- pages/_greeting.phtml -->
<p>Welcome to index97. Files are routes. No config needed.</p>
/* pages/public/style.css */
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
nav a { margin-right: 1rem; }

4. Run it

bun server.js

Open http://localhost:3000. Edit any file — the browser updates instantly.


Phase 2 — Level up

Dynamic routes

Wrap a folder name in brackets to make it a parameter.

pages/
  blog/
    [slug].js       ← handles /blog/hello-world
    [slug].phtml    ← template for the above
// pages/blog/[slug].js
import db from './_db.js'

export async function GET(req) {
  const post = db.query('SELECT * FROM posts WHERE slug = ?').get(req.params.slug)
  if (!post) return new Response('', { status: 404 })
  return { post }
}
<!-- pages/blog/[slug].phtml -->
<h1>{{post.title}}</h1>
<p>{{post.body}}</p>

Templates

| Syntax | What it does | |--------|-------------| | {{name}} | Render value, HTML-escaped | | {{{name}}} | Render value, raw HTML | | {{#if name}}...{{/if}} | Conditional | | {{#each items}}...{{/each}} | Loop — {{this}} is each item | | <include src="partial.phtml"> | Server-side partial | | <include src="partial.phtml" label="@item.label"> | Pass data to partial |

Layout slots

Pages can inject into named slots in the layout:

<!-- in any page -->
<template data-slot="title">About — My Site</template>
<template data-slot="head">
  <link rel="stylesheet" href="/about.css">
</template>

<h1>About</h1>
<!-- in _layout.html -->
<title>{{slot:title || My Site}}</title>
{{slot:head}}
{{content}}

Server-side layout data

Export a data function from _layout.js to make values available across every page — useful for navigation, session state, feature flags:

// pages/_layout.js
export function data(req) {
  const session = getSession(req)
  return { session }
}
<!-- in _layout.html -->
{{#if session}}<a href="/signout">Sign out</a>{{/if}}

Forms with PUT / PATCH / DELETE

Forms only support GET and POST natively. index97 rewrites the others automatically:

<form method="DELETE" action="/posts/42">
  <button>Delete</button>
</form>

Export the matching method from your handler:

export async function DELETE(req) {
  db.run('DELETE FROM posts WHERE id = ?', [req.params.id])
  return Response.redirect('/posts', 303)
}

Static site generation

Export staticPaths() from any dynamic handler to tell the build which URLs to render:

// pages/blog/[slug].js
export function staticPaths() {
  return db.query('SELECT slug FROM posts').all().map(p => ({ slug: p.slug }))
}
bunx index97 build   # renders all routes to dist/
bunx index97 serve   # serves dist/ as a static site

Composing multiple apps on one port

createRoutes() separates route discovery from server creation. Use it when you want to run two apps — say, a website and a chat service — on the same port without a proxy.

// server.js
import { createServer, createRoutes } from '@devchitchat/index97'

// Build routes for a second app, prefixed at /chat
const chatRoutes = await createRoutes({
  pagesDir: './chat/pages',
  prefix: '/chat',
  csp: "default-src 'self'; connect-src 'self' ws: wss:",
})

// Add any explicit routes the second app needs
chatRoutes['/chat/ws'] = (req, server) => {
  if (server.upgrade(req)) return
  return new Response('WebSocket upgrade required', { status: 426 })
}

// Single server — website at / and chat at /chat
const server = await createServer({
  pagesDir: './pages',
  port: 3000,
  routes: chatRoutes,          // merged in alongside the website's own routes
  websocket: chatWebsocket,    // from the second app
})

createRoutes(options)

Discovers routes from a pagesDir and returns a Bun-compatible routes object. All patterns are optionally prefixed so they don't collide with the host app's routes.

| Option | Type | Default | Description | |---|---|---|---| | pagesDir | string | — | Directory to discover routes from | | prefix | string | "" | URL prefix prepended to every route pattern | | dev | boolean | false | Inject HMR script into HTML responses | | csp | string | default CSP | Content-Security-Policy header value | | permissionsPolicy | string | camera=(), microphone=(), geolocation=() | Permissions-Policy header value | | notFoundPage | string | null | Path to a custom 404 page |

The returned object is a plain Record<string, Function> — pass it directly to createServer() via the routes option, or spread it with other explicit routes before passing.

Static files in pagesDir/public/ are served by createServer()'s built-in fetch handler and are not included in the returned routes object. If the second app's static files need to be served under a prefix, register them as explicit routes:

const glob = new Bun.Glob('**/*')
for await (const file of glob.scan({ cwd: './chat/pages/public', onlyFiles: true })) {
  chatRoutes['/chat/' + file] = () => new Response(Bun.file('./chat/pages/public/' + file))
}

CLI

| Command | What it does | |---------|-------------| | bunx index97 | Start dev server with HMR | | bunx index97 start | Start production server | | bunx index97 build | Generate static site to dist/ | | bunx index97 serve | Serve a pre-built dist/ |

Project layout

my-site/
  server.js           ← entry point
  pages/
    _layout.html      ← wraps every page
    _layout.js        ← server-side data for the layout
    index.html        ← /
    about.html        ← /about
    blog/
      index.html      ← /blog
      [slug].js       ← /blog/:slug  (handler)
      [slug].phtml    ← template for the handler
    public/
      style.css       ← served as static files
      logo.png

Files starting with _ are private — they are not routes.


Security

Content Security Policy

index97 sets the following CSP header on every response by default:

default-src 'self'; style-src 'self'; script-src 'self'

This means inline styles and inline scripts are blocked. Use external stylesheets and script files served from pages/public/ instead.

<!-- blocked -->
<div style="color: red">hello</div>
<style>body { margin: 0 }</style>

<!-- allowed -->
<link rel="stylesheet" href="/style.css">

To override the CSP for the entire server, pass a csp option to createServer:

import { createServer } from '@devchitchat/index97'
createServer({
  pagesDir: './pages',
  csp: "default-src 'self'; style-src 'self' 'unsafe-inline'"
})

To override the CSP for a single route, set Content-Security-Policy on the response returned by the handler — index97 will leave it untouched:

Bun.serve({
  routes: {
    '/embed': {
      GET: () => new Response('ok', {
        headers: { 'Content-Security-Policy': "frame-ancestors 'self' https://example.com" }
      })
    }
  }
})

The same pattern works for Permissions-Policy.