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

@ripetchor/vivi

v0.0.13

Published

WARNING: This package is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.

Readme

Vivi

WARNING: This package is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.

Primitive Validators

  • v.string(options?)
  • v.number(options?)
  • v.boolean(options?)
  • v.bigint(options?)
  • v.null(options?)
  • v.undefined(options?)

Other Validators

  • v.unknown()
  • v.fallback(validator, defaultValue)
  • v.email(options?)
  • v.nullable(validator)
  • v.optional(validator)
  • v.literal(value, options?)
  • v.date(options?)
  • v.union([validators], options?)
  • v.instance(Class, options?)
  • v.object(shape, options?)
  • v.array(validator, options?)
  • v.map(keyValidator, valueValidator, options?)
  • v.set(validator, options?)
  • v.refine(validator, refinementFns[], options?)
  • v.pipe(validator, ...transformFns)

Usage Examples

import { v } from '@ripetchor/vivi'
// or 
import v from '@ripetchor/vivi'
 // or 
import { stringValidator, numberValidator, refine /* etc. */ } from '@ripetchor/vivi'

Primitive validation

const name = v.string().parse('Alice');
console.log(name); // Alice

const age = v.number().parse(30);
console.log(age); // 30

const active = v.boolean().parse(true);
console.log(active); // true

Nullable and optional

const nullableName = v.nullable(v.string()).parse(null);
console.log(nullableName); // null

const optionalAge = v.optional(v.number()).parse(undefined);
console.log(optionalAge); // undefined

Array validation

const numbers = v.array(v.number()).parse([1, 2, 3]);
console.log(numbers); // [1, 2, 3]

const users = v.array(v.object({ name: v.string(), age: v.number() })).parse([
  { name: "Lana", age: 28 },
  { name: "Mikaela", age: 25, friends: ["Nicole"] },
  { name: "Elsa", age: 28, job: "dev" },
]);
console.log(users); // [{ name: 'Lana', age: 28 }, { name: 'Mikaela', age: 25 }, { name: 'Elsa', age: 28 }]

Object validation

const exactUser = v.object({ name: v.string(), age: v.number() }, { mode: 'exact' }).parse({ name: 'Alice', age: 25 });
console.log(exactUser); // { name: 'Alice', age: 25 }

const stripUser = v.object({ name: v.string() }, { mode: 'strip' }).parse({ name: 'Bob', age: 30 });
console.log(stripUser); // { name: 'Bob' }

const passthroughUser = v.object({ name: v.string() }, { mode: 'passthrough' }).parse({ name: 'Charlie', age: 40 });
console.log(passthroughUser); // { name: 'Charlie', age: 40 }

Union validation

const value = v.union([v.string(), v.number()]).parse('Hello');
console.log(value); // 'Hello'

Literal validation

const l1 = v.literal("yes").parse("yes");
console.log(l1); // 'yes'

v.literal("yes").parse("yeS");
// error

Refinements

const schema = refine(
  v.string(),
  [
    (value) => value.length === 5 || "length must be 5",
    (value) => /^\d+$/.test(value) || "contains only numbers",
  ],
  { abort: true },
);

const result = schema.safeParse("abc");

if (!result.success) {
  console.log(result.error.issues); // [ { path: [], message: 'length must be 5' } ]
} else {
  console.log(result.data);
}

// =========================================

const userSchema = v.object({
  password: v.string(),
  confirmPassword: v.string(),
});

const refinedUserSchema = v.refine(userSchema, [
  (user) =>
    user.password === user.confirmPassword || {
      path: ["confirmPassword"],
      message: "Passwords do not match",
    },
]);

const result = refinedUserSchema.safeParse({
  password: "secret123",
  confirmPassword: "secret321",
});
console.log(result); // [ { path: ["confirmPassword"], message: "Passwordsdo not match"} ]
  • true → validation passes
  • false → generic "Refinement failed"
  • string → error message with empy path
  • { message, path }precise error targeting

Fallback

const fallbackValue = v.fallback(v.number(), 42).parse('invalid');
console.log(fallbackValue); // 42

Email validation

const email = v.email().parse('[email protected]');
console.log(email); // '[email protected]'

Date validation

const birthday = v.date().parse(new Date('1990-01-01'));
console.log(birthday); // 1990-01-01T00:00:00.000Z

Pipe transformations

const transformed = v.pipe(
  v.string(),
  (s: string) => s.trim(),
  (s: string) => s.toUpperCase(),
).parse(' hello ');
console.log(transformed); // 'HELLO'

Safe parsing

const result1 = v.number().safeParse(123);
console.log(result1); // { success: true, data: 123 }

const result2 = v.number().safeParse('not a number');
console.log(result2); // { success: false, error: ViviError }

Abort on first issue

try {
  v.array(v.number(), { abort: true }).parse([1, "x", 3, "y"]);
} catch (err) {
  if (err instanceof ViviError) {
    console.log("Aborted on first error:", err.issues);
  }
}

Pattern matching (match)

match is a alternative to if / else for handling the result of safeParse.

  • enforces handling of all possible states

  • provides proper type narrowing

  • eliminates if (result.success) checks

const schema = v.string();

const result = schema.safeParse("hello");

v.match(
  result,
  (data) => {
    console.log("Success:", data);
  },
  (error) => {
    console.log("Validation failed:", error.issues);
  },
);

// Еhe return type is inferred from the callbacks.
const value = v.match(
  result,
  (data) => data.toUpperCase(),
  () => "DEFAULT",
);
// value: string

Type inference

const userValidator = v.object({ name: v.string(), age: v.number() });
type User = v.infer<typeof userValidator>;

const user: User = { name: 'Alice', age: 25 };
console.log(user); // { name: 'Alice', age: 25 }

Issues utils

import { v, flattenErrors, issuesToObject, issuesToString } from "@ripetchor/vivi";

const schema = v.object({
  user: v.object({
    name: v.string(),
    email: v.email(),
    address: v.object({
      street: v.string(),
      city: v.string(),
      zip: v.string(),
    }),
  }),
  credentials: v.object({
    password: v.string(),
    confirmPassword: v.string(),
  }),
  agreeToTerms: v.literal(true),
});

const invalidData = {
  user: {
    name: 1,
    email: 2,
    address: {
      street: 3,
      city: 4,
      zip: 5,
    },
  },
  credentials: {
    password: 6,
    confirmPassword: 7,
  },
  agreeToTerms: false,
};

const result = schema.safeParse(invalidData);

console.log(issuesToString(result.error.issues));
// user.name: Expected string
// user.email: Expected string
// user.address.street: Expected string
// user.address.city: Expected string
// user.address.zip: Expected string
// credentials.password: Expected string
// credentials.confirmPassword: Expected string
// agreeToTerms: Expected literal true

console.log(issuesToObject(result.error.issues));
// {
//   user: {
//     name: 'Expected string',
//     email: 'Expected string',
//     address: {
//       street: 'Expected string',
//       city: 'Expected string',
//       zip: 'Expected string'
//     }
//   },
//   credentials: { password: 'Expected string', confirmPassword: 'Expected string' },
//   agreeToTerms: 'Expected literal true'
// }

console.log(flattenErrors(result.error.issues));
// {
//   'user.name': 'Expected string',
//   'user.email': 'Expected string',
//   'user.address.street': 'Expected string',
//   'user.address.city': 'Expected string',
//   'user.address.zip': 'Expected string',
//   'credentials.password': 'Expected string',
//   'credentials.confirmPassword': 'Expected string',
//   agreeToTerms: 'Expected literal true'
// }

Custom validator

import { customValidator, ViviError, type ParseFn, type Path } from '@ripetchor/vivi';


interface User {
  name: string;
  age: number;
  email: string;
}

const parseFn: ParseFn<User> = (input: unknown, path: Path): User => {
  if (typeof input !== "object" || input === null) {
    throw ViviError.single("Expected object", path);
  }

  const obj = input as Record<string, unknown>;

  const name = obj.name;
  if (typeof name !== "string" || name.trim() === "") {
    throw ViviError.single("Name must be a non-empty string", [...path, "name"]);
  }

  const age = obj.age;
  if (typeof age !== "number" || age < 0) {
    throw ViviError.single("Age must be a non-negative number", [...path, "age"]);
  }

  const email = obj.email;
  if (typeof email !== "string" || !/^\S+@\S+\.\S+$/.test(email)) {
    throw ViviError.single("Invalid email", [...path, "email"]);
  }

  return { name: name.trim(), age, email };
};

const userValidator = customValidator<User>(parseFn);

const result = userValidator.safeParse({ name: " Alice ", age: 25, email: "[email protected]" });

if (result.success) {
  console.log(result.data); // { name: "Alice", age: 25, email: "[email protected]" }
}