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

calendaryjs

v0.9.0

Published

Composable calendar & recurrence engine with pluggable calendar systems.

Downloads

3,246

Readme

  • 🔁 Recurrence engine — daily, weekly, monthly, nth-weekday, formula, relative
  • 🔌 Pluggable calendar systems — one engine, every calendar
  • ✍️ Fluent builderevery("week").on("mon", "wed", "fri")
  • 🔍 Query API — filter events across groups & date ranges
  • 🧩 Collections (.cdy) + ICS export — plain, serializable data
  • 🪶 Zero-dependency · ~7KB gzip · browser · Node · edge · RN

Overview

calendaryjs turns event rules into dated occurrences. You declare events (Christmas, every other Tuesday, the 2nd Sunday of May…), the engine expands their recurrence into concrete dates, and you read, query, or export the result. The core speaks the Gregorian calendar; plugins teach it others — lunar, hijri, liturgical, or your own.

declare (builder) → expand (engine) → read · query · export

What it isn't. calendaryjs is the expansion engine — rules → dated occurrences as plain data. It is timezone-neutral, plain-date (YYYY-MM-DD): ideal for all-day / annual events, not a meeting scheduler (time-of-day + cross-timezone resolution is the host's job). It doesn't persist your data, render the calendar, or deliver notifications (reminders are pre-computed offsets, not fired). Licensed PolyForm Noncommercial — commercial use needs a separate license. Wire the rest to your own stack.

Install

npm i calendaryjs

1 · Your first event

Declare events, then read them back as dated occurrences:

import { calendary } from "calendaryjs";
import { every, date } from "calendaryjs/builder";

const cal = calendary();
cal.add(every("year").on(date(12, 25)).title("Christmas"));

cal.getEvents("2026-12-25");
// → [{ title: "Christmas", date: "2026-12-25", type: "const", … }]

To organize events into named groups you can toggle or export separately, use cal.addGroup({ id: "holidays", events: [...] }) instead.

2 · Recurring events — the builder

Every declaration reads like a sentence — frequency → position → payload — and compiles to a plain, serializable config:

import { every, once, date, nth } from "calendaryjs/builder";

every("year").on(date(1, 1)).title("New Year"); // Jan 1, every year
every("year")
  .on(nth(4, "thursday", "nov"))
  .title("Thanksgiving");
every(2, "weeks").on("tuesday").title("Standup"); // every other Tuesday
every("week").on("monday", "wednesday", "friday").title("Gym"); // pick weekdays
every("month").on(nth(1, "monday")).title("Retro"); // 1st Monday of every month
every("month").on(nth(-2, "friday")).title("Review"); // 2nd-to-last Friday
every("month").on(-1).title("Rent"); // last day of every month
every(3, "days").title("Water plants"); // every 3 days
every(2, "years").on(date(6, 1)).starting(2025).title("Reunion"); // every other year
every("week").on("tuesday").starting("2026-09-01").times(10).title("Course"); // ends after 10
once("2026-06-15").title("Wedding"); // a single date

Prefer raw objects? The builder just build()s into a plain config — hand-write it if you like; both compile to the same model, and describe(config) renders a config back as its builder sentence for debugging. Full grammar: the builder guide.

3 · Read & query

cal.getEvents("2026-12-25"); // a single day
cal.getEventsInRange("2026-01-01", "2026-12-31"); // a range
cal.search().group("holidays").range("2026-01-01", "2026-12-31").getEvents();

// Filter by kind, status, or source (each matches any of the listed values).
// Every search sets a window — .range(), .date(), .year(), or .month().
cal.search().type("weekly").status("confirmed").source("work-feed").year(2026).getEvents();

4 · Add a calendar system

A plugin extends the engine and the builder's vocabulary — install it, use() it, and new event types become available:

import { lunar } from "calendaryjs-plugin-lunar";

const cal = calendary().use(lunar());
cal.addGroup({
  id: "lunar",
  events: [every("year").on(lunar.date(1, 1)).title("Lunar New Year")],
});

| Plugin | Adds | | -------------------------------------------------------------------------------------------- | ------------------------------------------ | | calendaryjs-plugin-lunar | Lunar (lunisolar) date conversion + events | | calendaryjs-plugin-hijri | Islamic (Hijri) date conversion + events | | calendaryjs-plugin-liturgical | Easter computus, movable feasts, seasons |

5 · Bundle & share — collections

Bundle events into a portable collection (a .cdy file is its JSON form) and load it with cal.load(). It declares the plugins its events need; register those first — load() validates them and errors clearly if any is missing.

const cal = calendary().use(lunar());

cal.load({
  collection: "holidays",
  plugins: ["calendaryjs-plugin-lunar"],
  events: [
    { type: "const", id: "new-year", month: 1, day: 1, title: "New Year" },
    { type: "lunar", id: "tet", lunarMonth: 1, lunarDay: 1, title: "Lunar New Year" },
  ],
});

cal.load(jsonString); // …or from a .cdy (JSON) string, read from disk or fetched

The reverse — cal.toCollection() — serializes events back out, declaring the plugins their types need so the result re-loads cleanly. JSON.stringify it for a .cdy file:

const doc = cal.toCollection({ name: "holidays" });
// → { collection: "holidays", plugins: ["calendaryjs-plugin-lunar"], events: [ … ] }

JSON.stringify(doc); // the .cdy form — write to disk, or POST it somewhere

Validate a .cdy (editors + AI). A JSON Schema ships with the package (calendaryjs/schema/cdy.schema.json) and is published at https://calendaryjs.dev/schema/cdy.json. Add it to any collection for autocomplete + on-the-spot validation in VS Code — and so an AI generating a collection self-corrects:

{
  "$schema": "https://calendaryjs.dev/schema/cdy.json",
  "collection": "holidays",
  "events": [{ "type": "const", "id": "ny", "month": 1, "day": 1, "title": "New Year" }]
}

Building tooling on top of calendaryjs? See /llms.txt — a compact, full API reference written for LLMs/agents.

More examples — copy-paste recipes, simplest first (a first calendar → recurring patterns → milestone anniversaries → composing collections): calendaryjs.dev/examples.

Reference

Event types

| Type | Description | Example | | ------------- | ------------------------------------------------- | --------------------------- | | const | Fixed annual date (negative day = from month end) | Christmas (Dec 25) | | fixed | One-time date | Wedding (Jun 15, 2025) | | monthly | Same day every month (interval; -1 = last day) | Payday (15th) · Rent (-1) | | weekly | One or more weekdays (interval/range) | Standup (Mon, Wed, Fri) | | daily | Every day, or every N days | Water plants (every 3 days) | | nth-weekday | Nth weekday of a month (±1–±5) — or every month | 1st Monday of every month | | formula | Custom formula | Last business day of May | | relative | Offset from a registered anchor | 49 days after an anchor |

Plugins add more types (e.g. lunar, hijri, liturgical).

Event properties

Every event shares BaseEventProperties plus its type-specific date fields. id, type, and title are required; everything else is optional.

| Property | Type | Notes | | ---------------------------------- | ------------------------------------- | ---------------------------------------------------- | | id · type · title | string | required | | description · location · url | string | display metadata | | icon · color | string | per-event motif | | allDay | boolean | — | | startTime · endTime | string | "09:00""17:00" | | duration | number | minutes | | keywords · categories | string[] | classification | | status | confirmed \| tentative \| cancelled | — | | priority | number | z-index on a shared day; higher = on top (default 0) | | source | string | origin: plugin / feed / ICS subscription | | metadata | Record<string, unknown> | arbitrary per-event data | | reminders | Reminder[] | lead-time offsets (→ ICS VALARM) | | startDate · endDate | string | validity window, inclusive (ICS DTSTART/UNTIL) | | count | number | end after N occurrences (RRULE COUNT) | | onConflict | ConflictResolver | same-day conflict: keep / drop / reschedule | | overrideDates | OverrideDatesMap | force-move an occurrence to another date | | exceptions | EventExceptions | per-occurrence skip / override (ICS EXDATE) |

Rescheduling & conflicts

Three optional directives reshape where a recurring event's occurrences land. All are applied at generation time and keyed by the originally computed date (YYYY-MM-DD), in this order: countexceptionsoverrideDatesonConflict.

overrideDates — unconditionally move specific occurrences, when you already know the exact dates to shift:

{
  type: "monthly", id: "payday", day: 1, title: "Payday",
  // The 1st is closed in these two months → pay on the previous day.
  overrideDates: { "2026-01-01": "2025-12-31", "2026-05-01": "2026-04-30" },
}

onConflict — decide per occurrence at runtime: keep, drop, or reschedule. Pass a function (evaluated for every date) or a date-keyed map:

{
  type: "weekly", id: "standup", dayOfWeek: 1, title: "Standup", // Mondays
  onConflict: date =>
    closedDays.has(date) ? { action: "reschedule", to: nextOpenDay(date) } : { action: "keep" },
}

exceptions — edit a single occurrence in place (ICS EXDATE / RECURRENCE-ID): { skip: true } drops it, { override: { … } } changes just that instance:

{
  type: "weekly", id: "class", dayOfWeek: 3, title: "Class", // Wednesdays
  exceptions: {
    "2026-07-01": { skip: true },
    "2026-07-08": { override: { title: "Class (guest speaker)" } },
  },
}

License

PolyForm Noncommercial 1.0.0 — free for any noncommercial purpose. Commercial use (incl. ad-supported sites) requires a commercial license — see Commercial licensing.