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

xcvzmoon-tools

v0.0.2

Published

A collection of reusable tools by xcvzmoon

Readme

xcvzmoon-tools

license CI npm version npm downloads

A collection of tools created to simplify what I personally use day-to-day, shared here in case they're useful to others too.

Installation

npm install xcvzmoon-tools

Tools

string-case

String case conversion utilities with full TypeScript literal-type inference ("fooBar" typed as "Foo-Bar" after trainCase, not just string).

Ported from unjs/scule. See Credits.

import {
  splitByCase,
  pascalCase,
  camelCase,
  kebabCase,
  snakeCase,
  trainCase,
  titleCase,
  flatCase,
  upperFirst,
  lowerFirst,
} from "xcvzmoon-tools/string-case";

splitByCase("foo_bar-baz/qux"); // ["foo", "bar", "baz", "qux"]
pascalCase("foo_bar-baz/qux"); // "FooBarBazQux"
camelCase("foo_bar-baz/qux"); // "fooBarBazQux"
kebabCase("fooBarBaz"); // "foo-bar-baz"
snakeCase("fooBarBaz"); // "foo_bar_baz"
trainCase("fooBarBaz"); // "Foo-Bar-Baz"
titleCase("foo-bar"); // "Foo Bar"
flatCase("foo-bar-baz"); // "foobarbaz"
upperFirst("foo"); // "Foo"
lowerFirst("Foo"); // "foo"

runtime-config

A portable, typesafe runtime config in the spirit of Nuxt/Nitro's runtimeConfig/useRuntimeConfig(): call defineRuntimeConfig() once at startup with a nested defaults object, then read the resolved result from anywhere in your app with useRuntimeConfig(). Unlike a hand-rolled Bun.env/process.env reader, it works with any env source (Node, Bun, Deno, or import.meta.env in a Vite/browser app), throws on invalid values instead of silently coercing to NaN/false, and supports required fields and array leaves.

Quick start

// e.g. server/runtime-config.ts — run once, at app startup
import { defineRuntimeConfig, required, envKey } from "xcvzmoon-tools/runtime-config";

defineRuntimeConfig(
  {
    node: { env: envKey("NODE_ENV", "development") }, // explicit name, bypasses the derived key and prefix
    db: {
      hostname: "localhost",
      port: 5432,
      tls: false,
      allowedOrigins: [] as string[], // "a.com,b.com" or '["a.com","b.com"]'
    },
    betterAuth: {
      secret: required(""), // throws at resolve-time if the env var is missing
    },
  },
  {
    prefix: "APP", // optional; derives APP_DB_HOSTNAME, APP_DB_PORT, etc.
    source: process.env, // optional explicit source; auto-detected if omitted
    dts: true, // writes ./runtime-config.d.ts with the RuntimeConfigSchema augmentation
  },
);
// anywhere else in your app
import { useRuntimeConfig } from "xcvzmoon-tools/runtime-config";

const config = useRuntimeConfig();
config.db.hostname; // string

defineRuntimeConfig() resolves eagerly (so a missing required value fails your app at startup, not on the first request that happens to touch it) and stores the result in module state. useRuntimeConfig() reads that state — call it as many times, from as many files, as you like. Calling defineRuntimeConfig() a second time simply overwrites the previous result.

Defining a schema

A schema is a plain nested object. Every leaf's value is both its default and the thing that decides its type and its coercion rule:

| Leaf default | TS type | Env var coercion | | ----------------- | ---------- | --------------------------------------------------------------------- | | "localhost" | string | used as-is | | 5432 | number | Number(raw), throws if NaN | | false | boolean | only "true" / "false" accepted, else throws | | [] as string[] | string[] | JSON.parse(raw) if valid JSON array, else raw.split(","), trimmed | | [0] as number[] | number[] | same as above, then each item run through the number rule |

Nest plain objects as deep as you like — anything that isn't a Leaf (string/number/boolean/array) is treated as a nested group, not a value:

defineRuntimeConfig({
  services: {
    email: { host: "smtp.example.com", port: 587 },
    storage: { bucket: "", region: "us-east-1" },
  },
});

An empty array default ([] as string[]) can't tell you its element type at runtime, so it's always treated as string[] — seed it with a placeholder element cast away ([0] as number[], as above) if you need a number[] default that starts empty-feeling.

Env var naming

By default, an env var name is derived from the object path: segments are joined and run through this package's own snakeCase (from the string-case tool) and uppercased.

| Path | Derived env var | | --------------------- | --------------------- | | db.hostname | DB_HOSTNAME | | betterAuth.secret | BETTER_AUTH_SECRET | | services.email.host | SERVICES_EMAIL_HOST |

Pass prefix in the options to prepend a (also snake-cased + uppercased) namespace to every derived name — { prefix: "APP" } turns DB_HOSTNAME into APP_DB_HOSTNAME.

Some env vars don't follow the path convention at all (NODE_ENV, PORT, DATABASE_URL, ...). Use envKey(name, value) to pin the exact name for a single leaf, bypassing both the derived name and the prefix:

defineRuntimeConfig(
  { node: { env: envKey("NODE_ENV", "development") } },
  { prefix: "APP" }, // ignored for this leaf — reads NODE_ENV, not APP_NODE_ENV
);

required()

Wrap a leaf's default in required() to make defineRuntimeConfig() throw if the matching env var isn't supplied — regardless of whatever placeholder default you passed (the placeholder only exists to give TypeScript a type to infer):

defineRuntimeConfig({ betterAuth: { secret: required("") } });
// throws: Missing required config value at "betterAuth.secret" (expected env var "BETTER_AUTH_SECRET")

required() and envKey() compose in either order — both of these are equivalent:

required(envKey("PORT", 3000));
envKey("PORT", required(3000));

Validation and coercion

Every leaf is coerced according to the table in Defining a schema. Invalid values throw a RuntimeConfigError naming the config path, the env var, and the offending raw value — nothing is silently corrupted into NaN or false:

Invalid value for "db.port" (env "DB_PORT" = "not-a-number"): expected a number

Env vars that don't correspond to any key in your schema are ignored.

Options

The second argument to defineRuntimeConfig(schema, options):

| Option | Type | Default | Description | | -------- | ------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- | | prefix | string | none | Prepended (snake-cased + uppercased) to every derived env var name. Ignored by envKey() leaves. | | source | Record<string, string \| undefined> | auto-detected (see below) | Explicit env source to read from instead of auto-detection. | | dts | string \| boolean | undefined (no file written) | Writes a RuntimeConfigSchema augmentation. true./runtime-config.d.ts; a string → that path. |

Env sources / portability

When source isn't given, the env source is auto-detected in this order:

  1. process.env — covers Node, Bun, and Deno (via its Node-compat layer).
  2. import.meta.env — Vite/browser apps.
  3. {} — no source available; every leaf falls back to its default.

This means the same schema works unchanged whether your app runs on Node, Bun, Deno, or gets bundled by Vite for the browser.

Typing useRuntimeConfig()

defineRuntimeConfig() and useRuntimeConfig() are usually called from different files, and TypeScript types don't survive that trip on their own — there's no way for a zero-argument useRuntimeConfig() imported elsewhere to automatically know what schema a different file passed to defineRuntimeConfig(). Nuxt solves this the same way: nuxt dev/prepare inspects your resolved runtimeConfig and writes a declare module augmentation to disk, which is exactly what the dts: true option does for you — it writes:

// ./runtime-config.d.ts — generated, do not edit by hand
declare module "xcvzmoon-tools/runtime-config" {
  interface RuntimeConfigSchema {
    node: { env: string };
    db: { hostname: string; port: number; tls: boolean; allowedOrigins: string[] };
    betterAuth: { secret: string };
  }
}
export {};

Once that file is part of your tsconfig.json (it will be by default — restart your TS server/editor after the first run to pick it up), useRuntimeConfig() is typed everywhere with zero repeated generics. This works via TypeScript's declaration merging: interfaces with the same name (targeting the same module) get merged into one by the compiler, the same mechanism libraries like vue-router/fastify/express use for typed globals.

Two alternatives to the dts option:

  • Hand-write the augmentation yourself, anywhere in your project (e.g. a types/runtime-config.d.ts you commit):
    declare module "xcvzmoon-tools/runtime-config" {
      interface RuntimeConfigSchema {
        db: { hostname: string };
      }
    }
  • Skip augmentation entirely and pass an explicit type argument per call site: useRuntimeConfig<typeof schema>(). Useful when you don't want a global augmentation at all — e.g. a project that legitimately resolves several independent, differently-shaped configs.

The dts-writing step is Node-only and lazily loaded (node:fs/node:path are only ever imported if dts is actually set), so using this tool elsewhere (a Vite/browser bundle, an edge runtime) without the dts option never touches them.

Testing

resetRuntimeConfig() clears the resolved config — useRuntimeConfig() throws again until defineRuntimeConfig() is called again. Call it in a beforeEach to isolate tests that each call defineRuntimeConfig() with their own schema:

import { beforeEach } from "vitest";
import {
  defineRuntimeConfig,
  useRuntimeConfig,
  resetRuntimeConfig,
} from "xcvzmoon-tools/runtime-config";

beforeEach(() => resetRuntimeConfig());

test("reads DB_HOSTNAME", () => {
  const schema = { db: { hostname: "localhost" } };
  defineRuntimeConfig(schema, { source: { DB_HOSTNAME: "db.example.com" } });

  expect(useRuntimeConfig<typeof schema>().db.hostname).toBe("db.example.com");
});

Passing useRuntimeConfig's optional Schema generic (as <typeof schema> above) is what makes per-test ad-hoc schemas work without a global RuntimeConfigSchema augmentation — handy for a test suite that exercises many unrelated shapes, which is exactly what this tool's own test suite does.

resetRuntimeConfig() isn't just a test hack, either — it's also valid to call in legitimate hot-reload scenarios where you intentionally want to re-resolve against a changed source.

Errors

Every failure this tool raises is a RuntimeConfigError (a plain Error subclass, error.name === "RuntimeConfigError"), thrown synchronously from defineRuntimeConfig() or useRuntimeConfig():

| Scenario | Thrown from | | ---------------------------------------------------------- | ----------------------- | | A required() leaf's env var is missing | defineRuntimeConfig() | | An env var can't be coerced to its leaf's type | defineRuntimeConfig() | | useRuntimeConfig() called before defineRuntimeConfig() | useRuntimeConfig() |

What this doesn't do

This does not implement Nuxt's runtimeConfig.public client/server split. Stripping server-only keys out of a client bundle requires bundler cooperation that a plain utility library can't provide — nest your own public: {...} key by convention if you need that, and let your bundler enforce it.

Credits

  • unjs/sculestring-case is a direct port of scule's source and test suite, used under its MIT license. It's vendored in rather than depended on, purely to keep this package dependency-free. See THIRD-PARTY-NOTICES.md for the original license text.

License

MIT