Skip to content

Changelog

Every package is published to npm and versions independently — a core release doesn’t bump a plugin unless its peer range changes.

  • 56affb2: New defineEvent() authoring helper. An identity function that brands an inline event config so a plugin config’s extra fields don’t trip EventConfig’s excess-property check — while id / title / type stay required, and no cast is needed:

    cal.addGroup({
    id: "g",
    events: [defineEvent({ type: "lunar", id: "tet", title: "Tết", lunarMonth: 1, lunarDay: 1 })],
    });

    Prefer typing with a plugin’s own event interface where you have one; reach for this for quick inline configs.

  • d26bd5d: New calendaryjs/testing entry — a test-kit for plugin authors. datesIn(plugin, events, range) and occurrencesIn(...) run a plugin through the real engine and return the occurrences a query would show. This is the reliable way to test a plugin: calling a handler’s generate() in isolation misses the engine’s year±1 bucketing, so boundary-crossing dates only surface through the full pipeline. Dev-only import — a separate entry, never in a production bundle.

  • d636df6: addGroup() / load() now run each event through its handler’s validate() and throw if it returns false — a malformed config fails fast, with a clear error, instead of silently producing wrong or missing dates downstream. Every handler already defined validate; the engine simply never called it before.

    Behavior change: a config a handler considers invalid (e.g. a lunar event with lunarDay: 99, or a plugin event missing a required field) now throws at addGroup / load rather than being accepted. If you were relying on invalid configs being silently tolerated, fix the config or loosen the handler’s validate.

  • 976949c: Type-ergonomics at the adapter boundary — pure signature changes, no runtime cost. Driven by an external adapter mapping a Zod-validated domain model onto the engine.

    • Precisely-typed plugin configs are assignable without a cast. EventConfig’s open (plugin) arm no longer carries a [key: string]: unknown index signature. A plugin’s own interface (LunarEvent, OffsetEvent, a custom event type) has no index signature either, so requiring one rejected well-typed configs and forced as unknown as EventConfig[] at every addGroup/load. Now they flow straight in. Plugin-specific fields are read internally via a typed cast at the point of use.
    • Runtime-validated date strings need no cast. New DateInput = DateString | (string & {}). The consumer-supplied date maps — overrideDates, exceptions keys, onConflict map keys + reschedule.to — accept any string, so data from a validator / JSON / user input flows in without an as cast. DateString still types engine output (CalendarEvent.date, movedFrom, SkippedOccurrence.date), where the format is guaranteed.
    • toCollection() returns SerializedCollectionevents: EventConfig[], never Buildable. Serialization compiles builders away, so a consumer reads cdy.events[i].title with no narrowing. Type-only.
    • buildCollection(configs, options) (+ pluginManifest) — the stateless half of instance.toCollection: assemble a portable .cdy document from plain configs with no engine instance. Pass plugins to derive the manifest and schema to stamp $schema.
    • Docs: getEventsInRange() is documented as globally sorted (date asc, then priority desc, stable) across the whole range; metadata is documented as present on output exactly when the source config set it.

    Note (potential type-break for a minority): a fresh object literal with extra plugin fields assigned directly to EventConfig now trips excess-property checks — type it as the plugin’s own event type instead (e.g. LunarEvent), which is the pattern the change enables.

  • 53eba84: Multi-collection .cdy bundles. A single shareable file can now carry several named collections instead of flattening to one.

    • load() expands a bundle — pass { collections: Collection[] } (or its JSON) and each collection becomes its own group, keeping its id / priority / color. Plugins are validated per collection. A single Collection still loads exactly as before.
    • toBundle(options?) — the inverse: exports every group as its own collection (id/name/priority/color + per-collection plugin manifest) into a SerializedCollectionBundle. toCollection() still flattens into one collection when that’s what you want.
    • Collection.color — a collection can now carry a group color that flows onto every event lacking its own (event-level wins), so color round-trips through a .cdy.
    • New types CollectionBundle / SerializedCollectionBundle. The published .cdy JSON Schema now accepts either a single collection or a { collections: [...] } bundle, and documents color.
  • 087dfb4: getSkipped() now mirrors the source event’s metadata onto each result, and SkippedOccurrence<TMetadata> is generic — matching the normal CalendarEvent path. A consumer that stores its domain object in metadata can render the skipped “ghost” occurrence from the same object, with no side sourceEventId → event lookup. Additive: metadata is optional and omitted when the source has none.
  • 161707d: Add six declarative, JSON-serializable recurrence/grouping primitives. All are optional and back-compatible — absent field / new output ⇒ today’s behavior.

    • const.keepOrdinals (+ shared origin): emit a yearly event only on selected anniversaries — year - (origin ?? startYear) in the list (e.g. [1, 5, 10, 25]). Requires an anchor; mutually exclusive with interval. For the arithmetic “every Nth year” case, keep using interval + startYear.
    • const.onMissingDay: "overflow" (default, Feb 29 → Mar 1), "clamp" (last valid day of the month), or "skip" when a positive day doesn’t exist that period.
    • Group/collection priority: a base priority for every event in a group/collection that lacks its own (event-level always wins) — addGroup({ priority }), Collection.priority, or load(cdy, { priority }). A collection-level z-index.
    • CalendarEvent.ordinal: the anniversary count occurrenceYear - (origin ?? startYear), present when the source event has an anchor — render “Nth / N years” with no math. Derived from the original computed year, not a moved date.
    • requiredPlugins(events?): the npm names of the registered plugins the events need (the derivation toCollection() already used, now public).
    • Occurrence provenance: CalendarEvent.movedFrom (the original date of a rescheduled occurrence) and getSkipped({ from, to }) (occurrences removed by an exceptions skip / EXDATE) — render moved/ghost markers without re-reading configs.

    The published .cdy JSON Schema gains origin / keepOrdinals / onMissingDay and collection-level priority.

  • 320b00c: search() now requires a bounded window; .year() / .month() sugar.

    Recurrences are infinite, so “search everything” has no meaning — previously an un-ranged search silently expanded a made-up 1970–2100 window (131 years of occurrences). An unbounded search now throws a teaching error, matching how rrule and the Google Calendar API require explicit bounds for expansion.

    Migration — add one clause to un-ranged searches:

    cal.search().text("christmas").getEvents(); // before (throws now)
    cal.search().text("christmas").year(2026).getEvents(); // after

    New SearchBuilder.year(y) and .month(y, m) bound the window to a calendar year/month (sugar for .range(...)). .dateFrom() / .dateTo() alone still leave one end open and therefore also throw — pair them, or use .range(). getEventsByGroup(id, from, to) now requires its range for the same reason.

  • 7b70407: Performance, flexibility, and authoring DX.

    Performance — per-year generation cache. Occurrences are now computed and cached per calendar year and range queries assemble from those buckets, instead of recomputing everything per from:to range key. A UI paging month-by-month through a year is ~8× faster; repeated mixed reads ~3×. Cache memory is bounded by years touched, not by distinct ranges. The unused EventIndex was removed from the hot path.

    count — end a series after N occurrences (RRULE COUNT). A new optional field on every event (builder: .times(n)), applied centrally so it works for core and plugin types alike. Needs a startDate or startYear (builder: .starting(...)) to count from.

    Yearly interval. const and nth-weekday events accept interval — every N years, phased from a required startYear (builder: every(2, "years").on(date(6, 1)).starting(2025)).

    Extended nth. nth-weekday now takes 1–5 from the start and -1–-5 counted from the end (nth(-2, "friday") = second-to-last Friday).

    cal.add(...events). Add events without the group ceremony — they land in a shared "events" group; repeated calls accumulate.

    cal.registerAnchor(name, fn). The anchor-flavoured name for registering the year → Date resolvers that relative events (builder from(anchor)) offset from. registerFormula still works; the missing-anchor error now points at registerAnchor.

    Fix. search().first() and .exists() no longer permanently set the builder’s limit to 1.

    The .cdy JSON Schema covers the new fields (count, yearly interval, extended nth).

  • 6359137: EventConfig’s open (plugin) arm now carries the full BaseEventProperties.

    Every event config — core or plugin — is guaranteed an id and title at the type level, and the shared optional fields (priority, startDate, count, exceptions, …) are typed instead of unknown. Previously the open arm was { type: string; [key: string]: unknown }, so an incomplete plugin config only failed at runtime. TypeScript consumers passing plugin-typed configs without id/title will now get a compile error (the engine always required both at runtime). The liturgical plugin’s internal config assembly is typed against the tightened arm — no behavior change.

  • 6359137: describe(config) — the inverse of build().

    New calendaryjs/builder export that renders a plain event config back as its canonical builder sentence (e.g. every("week").on("monday").id("gym").title("Gym")) — for debugging, logs, and reading stored .cdy events aloud. Returns null for shapes with no builder form (plugin event types, formula, anchored-mode nth-weekday, monthly with a startMonth phase). Round-trip is tested: re-evaluating the sentence with the real builder reproduces the original config.

  • a04fddd: Point homepage at the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs /license page and email contacts instead of links into the (private) GitHub repo.
  • dd3e09a: Ship llms.txt inside the package (calendaryjs/llms.txt) — a compact, self-contained API reference for coding agents that read dependency files directly, so it works offline without the docs site. (It’s also served at calendaryjs.dev/llms.txt once the site is live.)
  • f801167: Ship a JSON Schema for the .cdy collection format, for editor + AI tooling. The schema is bundled in the package (calendaryjs/schema/cdy.schema.json) and published at https://calendaryjs.dev/schema/cdy.json. Collections now accept an optional $schema field — add it to get autocomplete + on-the-spot validation in VS Code, and so an AI generating a .cdy self-corrects (it rejects the common mistakes, e.g. date instead of day, an out-of-range month, an invalid nth). A compact LLM-oriented reference also ships at /llms.txt.
  • 89125f3: Docs: fix the Thanksgiving example in the README. The builder’s nth() takes a 3-letter month name, so it’s nth(4, "thursday", "nov")"november" threw at runtime.
  • f13dd06: License change: the core calendaryjs is now licensed under the PolyForm Noncommercial License 1.0.0 (was MIT). It remains free for any noncommercial purpose; commercial use now requires a commercial license — see COMMERCIAL.md. The official plugins stay MIT but now ship their own LICENSE file and note that the combined work’s commercial use requires a core commercial license.

  • 8cb08c7: Round out the recurrence engine and query surface (all pure, zero-dependency core):

    New recurrence shapes

    • daily eventsevery("day") / daily() / every(N, "days"). The missing 4th RRULE frequency; interval > 1 is phased off a fixed anchor so “every N days” stays in step across the year boundary.
    • Nth weekday of every monthevery("month").on(nth(1, "monday")) (and the nth(n, weekday) 2-arg form). Omitting the month repeats it monthly instead of needing 12 events.
    • Negative day-of-monthday: -1 = last day of the month (monthly(-1)), -2 = second-to-last, for monthly and const.

    Query

    • Filter by type, status, and source: cal.search().type("weekly").status("confirmed").source("feed-x"). getDays({ types }) now applies its long-documented filter, and every computed CalendarEvent carries its originating type.

    Fixes

    • Cache no longer serves stale events from other ranges after a mutation (the dirty flag now evicts the whole cache, not just the first re-read range).
    • Occurrences computed from an adjacent year’s anchor (e.g. a relative offset crossing Jan 1 / Dec 31) are no longer dropped from narrow queries.
    • Year bounds (.between(year, year) / .until(year) / exceptYears) are now honoured centrally, so they apply to daily / weekly / monthly too — previously those bounds were silently ignored on interval types.

    ExportsEventException, EventExceptions, and DailyEvent types are now re-exported from the package root.

  • 8fa9303: Add cal.toCollection() — export events as a portable collection, the inverse of cal.load(). Serializes the plain event configs and derives a manifest of only the plugins their event types actually use, so the result round-trips back through load(). JSON.stringify it for a .cdy document. Supports { name, version, group } options.

  • f4eb2f9: Add a Socket security badge (plus npm version / zero-deps) to the README.
  • 38c23be: Fix the calendaryjs/builder subpath on the published package. It is now built as a tsup entry (dist/builder/) and mapped in publishConfig.exports, so import { every } from "calendaryjs/builder" works when installed from npm — previously only the root entry was built and exported, and the subpath 404’d.
  • 3caf42a: Polish the READMEs and add a centered logo (served via jsDelivr). The core README now leads with what calendaryjs is — a sentence-like recurrence builder you teach any calendar system via plugins — shows the returned event shape, and fixes the stale plugin example (cal.use(plugin), not a non-existent calendary.extend).
  • 99ee489: Update package metadata for the new repository home (vbilltran68/calendaryjs) and point npm-facing links at npm: homepage and the README cross-links resolve on npm, while repository/bugs track the new GitHub repo. Also drop Day.js references from the docs.
  • f76e10e: Weekly events now accept multiple weekdays. Set dayOfWeek to a list ({ type: "weekly", dayOfWeek: [1, 3, 5] }) to fire on each selected day every week — like an alarm’s “Repeat”. The builder takes them variadically: every("week").on("monday", "wednesday", "friday") and weekly("monday", "wednesday", "friday"). A single weekday is unchanged (dayOfWeek: number), so existing events keep working. With interval, all selected days share one reference week (ICS BYDAY semantics).
  • Initial public release — a lightweight, plugin-based calendar & recurrence engine.
  • a04fddd: Point homepage at the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs /license page and email contacts instead of links into the (private) GitHub repo.
  • f13dd06: License change: the core calendaryjs is now licensed under the PolyForm Noncommercial License 1.0.0 (was MIT). It remains free for any noncommercial purpose; commercial use now requires a commercial license — see COMMERCIAL.md. The official plugins stay MIT but now ship their own LICENSE file and note that the combined work’s commercial use requires a core commercial license.
  • ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
  • 3caf42a: Polish the READMEs and add a centered logo (served via jsDelivr). The core README now leads with what calendaryjs is — a sentence-like recurrence builder you teach any calendar system via plugins — shows the returned event shape, and fixes the stale plugin example (cal.use(plugin), not a non-existent calendary.extend).
  • 99ee489: Update package metadata for the new repository home (vbilltran68/calendaryjs) and point npm-facing links at npm: homepage and the README cross-links resolve on npm, while repository/bugs track the new GitHub repo. Also drop Day.js references from the docs.
  • Initial public release — Islamic (Hijri) calendar plugin for calendaryjs.
  • a04fddd: Point homepage at the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs /license page and email contacts instead of links into the (private) GitHub repo.
  • f13dd06: License change: the core calendaryjs is now licensed under the PolyForm Noncommercial License 1.0.0 (was MIT). It remains free for any noncommercial purpose; commercial use now requires a commercial license — see COMMERCIAL.md. The official plugins stay MIT but now ship their own LICENSE file and note that the combined work’s commercial use requires a core commercial license.
  • ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
  • 3caf42a: Polish the READMEs and add a centered logo (served via jsDelivr). The core README now leads with what calendaryjs is — a sentence-like recurrence builder you teach any calendar system via plugins — shows the returned event shape, and fixes the stale plugin example (cal.use(plugin), not a non-existent calendary.extend).
  • 99ee489: Update package metadata for the new repository home (vbilltran68/calendaryjs) and point npm-facing links at npm: homepage and the README cross-links resolve on npm, while repository/bugs track the new GitHub repo. Also drop Day.js references from the docs.
  • 976949c: OffsetEvent.baseEvent is now typed "easter" | (string & {}) instead of plain string — editors suggest "easter" (the only computed base) while the type stays open to any string. No runtime change.
  • 087dfb4: Split the offset date mechanism from the liturgical catalog semantics. OffsetEvent<TMetadata> is now lean — baseEvent/offsetDays with plain, optional metadata — so a consumer can use the Easter-relative date while carrying their own metadata (the handler never reads liturgical fields). The required-LiturgicalMetadata shape moves to the new LiturgicalOffsetEvent<TExtra>, which the built-in presets/LITURGICAL_EVENTS use.

    Note: if you typed a value as OffsetEvent expecting required season/rank/ vestmentColor, switch it to LiturgicalOffsetEvent.

  • 6359137: EventConfig’s open (plugin) arm now carries the full BaseEventProperties.

    Every event config — core or plugin — is guaranteed an id and title at the type level, and the shared optional fields (priority, startDate, count, exceptions, …) are typed instead of unknown. Previously the open arm was { type: string; [key: string]: unknown }, so an incomplete plugin config only failed at runtime. TypeScript consumers passing plugin-typed configs without id/title will now get a compile error (the engine always required both at runtime). The liturgical plugin’s internal config assembly is typed against the tightened arm — no behavior change.

  • a04fddd: Point homepage at the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs /license page and email contacts instead of links into the (private) GitHub repo.
  • f13dd06: License change: the core calendaryjs is now licensed under the PolyForm Noncommercial License 1.0.0 (was MIT). It remains free for any noncommercial purpose; commercial use now requires a commercial license — see COMMERCIAL.md. The official plugins stay MIT but now ship their own LICENSE file and note that the combined work’s commercial use requires a core commercial license.
  • ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
  • 3caf42a: Polish the READMEs and add a centered logo (served via jsDelivr). The core README now leads with what calendaryjs is — a sentence-like recurrence builder you teach any calendar system via plugins — shows the returned event shape, and fixes the stale plugin example (cal.use(plugin), not a non-existent calendary.extend).
  • 99ee489: Update package metadata for the new repository home (vbilltran68/calendaryjs) and point npm-facing links at npm: homepage and the README cross-links resolve on npm, while repository/bugs track the new GitHub repo. Also drop Day.js references from the docs.
  • Initial public release — Catholic liturgical calendar plugin (Easter computus + offset feasts).
  • 0bcbba2: Vietnamese calendar variant (âm lịch Việt Nam).

    The plugin now ships two national variants of the East Asian lunisolar calendar, selected like an Intl/Temporal calendar id:

    • "chinese" (default, unchanged behavior) — the standard 1900–2100 almanac table at 120°E / UTC+8.
    • "vietnamese" — astronomical computation at 105°E / UTC+7 via the Ho Ngoc Duc algorithm, the de-facto Vietnamese standard.

    These are distinct standards, not timezones: whenever a new moon falls between 23:00 and 24:00 Vietnam time the Vietnamese month starts a day earlier — occasionally moving Tết (1968, 1985, 2007, 2030) or a leap-month boundary (1984, 1985, 1987, 1995, 2031). E.g. 2019-03-17 is 12/2 âm lịch but 11/2 in the Chinese calendar.

    Opt in per plugin instance — applies to lunar events and day enrichment alike:

    const cal = calendary().use(lunar({ calendar: "vietnamese" }));

    A lunar event can also pin its own variant with a calendar field (builder: lunar.date(2, 12, { calendar: "vietnamese" })), which wins over the instance default — so a shared .cdy collection is self-describing and renders the same dates for every consumer. solarToLunar / lunarToSolar / isValidLunarDate / lunar.fromSolar take an optional { calendar } argument. Round-trip stability and the leap-month fallback hold for both variants; zero runtime dependencies preserved. The Chinese variant deliberately keeps the published table rather than re-running the astronomical algorithm at UTC+8 (which would drift from the official Chinese calendar on 155 days in 1900–2100).

  • 5222487: Add isLeapMonth to lunar events — and { leap: true } to the lunar.date() builder selector — to target a leap month (闰月). In years without that leap month the event falls back to the regular month, matching Temporal’s monthCode. New lunar.fromSolar(date) derives { lunarMonth, lunarDay, isLeapMonth } from a known solar date, so the flag is computed rather than guessed (e.g. a recurring giỗ from a death date).
  • a04fddd: Point homepage at the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs /license page and email contacts instead of links into the (private) GitHub repo.
  • f13dd06: License change: the core calendaryjs is now licensed under the PolyForm Noncommercial License 1.0.0 (was MIT). It remains free for any noncommercial purpose; commercial use now requires a commercial license — see COMMERCIAL.md. The official plugins stay MIT but now ship their own LICENSE file and note that the combined work’s commercial use requires a core commercial license.
  • ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
  • 3caf42a: Polish the READMEs and add a centered logo (served via jsDelivr). The core README now leads with what calendaryjs is — a sentence-like recurrence builder you teach any calendar system via plugins — shows the returned event shape, and fixes the stale plugin example (cal.use(plugin), not a non-existent calendary.extend).
  • 99ee489: Update package metadata for the new repository home (vbilltran68/calendaryjs) and point npm-facing links at npm: homepage and the README cross-links resolve on npm, while repository/bugs track the new GitHub repo. Also drop Day.js references from the docs.
  • Initial public release — lunar (lunisolar) calendar plugin for calendaryjs.