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

oxidejs

v0.1.10

Published

Vite/Rsbuild plugin. One build → dist/server.js + optional client. Server actions via *.server.ts.

Downloads

1,362

Readme

oxidejs

One build command → a deployable tree:

dist/
├── client/           # only if index.html exists
└── server.js         # ESM server bundle

preset: "celld" also writes dist/wrangler.jsonc with main: "./server.js".

v1 targets Vite and Rsbuild via unplugin. Other bundlers are out of scope for now.

Vite

// vite.config.ts
import { defineConfig } from "vite";
import oxide from "oxidejs/vite";

export default defineConfig({
  plugins: [oxide()],
});
// tsconfig.json
{ "extends": "oxidejs/tsconfig" }
vite build
node dist/server.js

Default preset is "fetch". No index.html → only dist/server.js. With index.html → client to dist/client/, then /__oxide/action (if you have a *.server.{ts,tsx,js,jsx} file) → src/server.ts (undefined continues) → static file → index.html for navigations. public/ is copied next to the client. Hashed assets get Cache-Control: immutable. No wrangler.jsonc.

oxide({
  preset: "celld",
  wrangler: { name: "my-app", compatibility_date: "2026-01-01" },
});

"celld" writes dist/wrangler.jsonc for celld, a self-hosted alternative to Cloudflare Workers, and skips asset serving (ASSETS does that).

Server actions

Install tacho if you use actions. Files named *.server.ts, *.server.tsx, *.server.js, or *.server.jsx are server-only. A client import is replaced with a tacho stub that POSTs /__oxide/action. The original module never enters the client graph. Only exports wrapped in action() become remote actions — any other export stays server-local and is not callable over the wire. Server and Vite SSR (import.meta.env.SSR === true) keep the real functions. Methods are <file>.<fn> (test.ping). Call useRequest() inside an action for the inbound Request. useCtx() is tacho ctx ({ req } plus anything middleware or createContext added). On preset: "celld", useEnv() and useFetchCtx() are the Worker env and ctx from fetch(request, env, ctx) — same values as useCtx().env / useCtx().fetchCtx. Return undefined from src/server.ts to fall through to static files. No server action files → the bundle does not import tacho.

// src/test.server.ts
import { action, useRequest } from "oxidejs";

export const who = action(async () => {
  return useRequest().headers.get("x-user");
});

export const ping = action(async () => "pong");

// a non-action export is never exposed over the wire
async function internalHelper() {
  /* server-only */
}

// src/client.ts
import { ping } from "./test.server";
console.log(await ping()); // "pong"

// src/server.ts
export default {
  fetch(request: Request) {
    if (new URL(request.url).pathname === "/api/ok") return new Response("ok");
  },
};

action() is identity — it only marks the export. Wrap async function* in it to stream over tacho SSE. oxidejs/tsconfig makes await ticks() typecheck. Pass { signal } last on any action to abort the fetch. Types come from the real *.server.ts, so declare the last argument there:

// src/test.server.ts
import { action } from "oxidejs";
import type { ActionOptions } from "oxidejs";

export const ticks = action(async function* (n: number, _opts?: ActionOptions) {
  for (let i = 0; i < n; i++) yield i;
});

// src/client.ts
import { ticks } from "./test.server";

const ac = new AbortController();
const stream = await ticks(10, { signal: ac.signal });
ac.abort();

vite dev and rsbuild dev serve the endpoint via middleware. actions: "http" (default) serves /__oxide/action; actions: "ws" uses a WebSocket instead (needs crossws; not with preset: "celld"). actions.sameOrigin defaults to true for both transports; set it to false only when you intentionally accept cross-origin requests. Set actions.path to move the endpoint. actionHeaders are static headers on the shared HTTP client and are ignored for WebSocket actions.

Rsbuild

// rsbuild.config.ts
import { defineConfig } from "@rsbuild/core";
import oxide from "oxidejs/rsbuild";

export default defineConfig({
  plugins: [oxide()],
});

Same factory as Vite: client stubs, /__oxide/action, and dist/server.js.

Options

| Option | Default | Notes | | ------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- | | preset | "fetch" | "fetch" or "celld" | | workerEntry | src/server.ts | Relative to project root | | outDir | dist | Output root | | clientDir | client | Must stay inside outDir | | wrangler.name | required if emitConfig | | | wrangler.compatibility_date | required if emitConfig | | | wrangler.compatibility_flags | — | optional | | wrangler.durable_objects | — | optional | | wrangler.migrations | — | optional | | wrangler.services | — | optional | | wrangler.vars | — | optional | | emitConfig | true on celld | Set false to skip wrangler.jsonc | | actions | "http" | "ws" needs crossws; object form: { transport, path, sameOrigin } (sameOrigin: true) | | actionHeaders | — | Static headers on the HTTP client | | middleware | [] | Default-exported production fetch middleware, run in order before actions and server entry |

middleware modules receive (request, { env, ctx }). They run in array order before actions, the server entry, and assets. Return a Response to stop the chain or undefined to continue.

// src/auth.ts
export default function auth(request: Request) {
  if (!request.headers.has("authorization")) {
    return new Response("Unauthorized", { status: 401 });
  }
}

// vite.config.ts
oxide({ middleware: ["./src/auth.ts"] });

This option applies to production builds; use Connect middleware in Vite or Rsbuild during development.

main is always ./server.js. assets is added only when index.html exists. Unknown wrangler keys fail at build time.

Non-goals

  • No wrangler dev / workerd emulation
  • No automatic celld deploy
  • No Node-builtin polyfills — Vite ssr.noExternal: true is a hard-fail for stray Node imports

Security

Asset serving (preset: "fetch")

The generated server serves static files from dist/client/ (or the public/ directory merged into it). These guards are active:

| Attack vector | Guard | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Traversal (%2e%2e/, ..%2f) | __rel() rejects paths containing .. segments. | | Double-slash (///etc/passwd) | __rel() rejects results that still start with / after slice(1). | | Null byte (%00, \0) | __rel() rejects paths containing null bytes before and after decodeURIComponent. | | Absolute path (/etc/passwd) | __rel() returns null for paths not starting with /. | | SPA fallback | Unknown paths → index.html, never a directory listing. | | clientDir escape | resolveOptions throws at build time if clientDir resolves outside outDir. | | Hashed assets | Files matching [-.][0-9a-f]{8,}.ext get Cache-Control: public, max-age=31536000, immutable. Other files are not cached by default. |

The generated __asset function uses path.join — not path.resolve — so a leading / in the relative path stays inside the asset root.

Server actions (*.server.{ts,tsx,js,jsx})

  • Server action code is never bundled into the client. Client imports are replaced with tacho stubs that POST the action endpoint (default /__oxide/action). The original source stays server-only.
  • Only action()-wrapped exports are exposed as RPC; other exports stay server-local.
  • The endpoint is POST-only. Non-POST requests return 405.
  • Method dispatch uses Object.hasOwn, blocking __proto__ / constructor walks.
  • Unknown or missing content-types → 415.
  • Body size capped at 1 MB by default (enforced on the actual body, not just Content-Length).
  • Batch requests capped at 20 items (both HTTP and WebSocket transports).

Host header

The generated dev server constructs request.url from req.headers.host. This is standard HTTP/1.1 behavior (same as Express, Hono, Koa, Node http). If your src/server.ts reads request.url to construct redirects, validate the host yourself — the framework cannot distinguish a legitimate host header from a malicious one. In production, your reverse proxy handles this.