Changelog
Every package is published to npm and versions independently — a core release doesn’t bump a plugin unless its peer range changes.
calendaryjs (core)
Section titled “calendaryjs (core)”Minor Changes
Section titled “Minor Changes”-
56affb2: New
defineEvent()authoring helper. An identity function that brands an inline event config so a plugin config’s extra fields don’t tripEventConfig’s excess-property check — whileid/title/typestay 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/testingentry — a test-kit for plugin authors.datesIn(plugin, events, range)andoccurrencesIn(...)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’sgenerate()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’svalidate()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 definedvalidate; 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 ataddGroup/loadrather than being accepted. If you were relying on invalid configs being silently tolerated, fix the config or loosen the handler’svalidate.
Minor Changes
Section titled “Minor Changes”-
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]: unknownindex 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 forcedas unknown as EventConfig[]at everyaddGroup/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,exceptionskeys,onConflictmap keys +reschedule.to— accept anystring, so data from a validator / JSON / user input flows in without anascast.DateStringstill types engine output (CalendarEvent.date,movedFrom,SkippedOccurrence.date), where the format is guaranteed. toCollection()returnsSerializedCollection—events: EventConfig[], neverBuildable. Serialization compiles builders away, so a consumer readscdy.events[i].titlewith no narrowing. Type-only.buildCollection(configs, options)(+pluginManifest) — the stateless half ofinstance.toCollection: assemble a portable.cdydocument from plain configs with no engine instance. Passpluginsto derive the manifest andschemato stamp$schema.- Docs:
getEventsInRange()is documented as globally sorted (date asc, thenprioritydesc, stable) across the whole range;metadatais 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
EventConfignow trips excess-property checks — type it as the plugin’s own event type instead (e.g.LunarEvent), which is the pattern the change enables. - Precisely-typed plugin configs are assignable without a cast.
-
53eba84: Multi-collection
.cdybundles. 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 itsid/priority/color. Plugins are validated per collection. A singleCollectionstill loads exactly as before.toBundle(options?)— the inverse: exports every group as its own collection (id/name/priority/color + per-collection plugin manifest) into aSerializedCollectionBundle.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.cdyJSON Schema now accepts either a single collection or a{ collections: [...] }bundle, and documentscolor.
Minor Changes
Section titled “Minor Changes”- 087dfb4:
getSkipped()now mirrors the source event’smetadataonto each result, andSkippedOccurrence<TMetadata>is generic — matching the normalCalendarEventpath. A consumer that stores its domain object inmetadatacan render the skipped “ghost” occurrence from the same object, with no sidesourceEventId → eventlookup. Additive:metadatais optional and omitted when the source has none.
Minor Changes
Section titled “Minor Changes”-
161707d: Add six declarative, JSON-serializable recurrence/grouping primitives. All are optional and back-compatible — absent field / new output ⇒ today’s behavior.
const.keepOrdinals(+ sharedorigin): 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 withinterval. For the arithmetic “every Nth year” case, keep usinginterval+startYear.const.onMissingDay:"overflow"(default, Feb 29 → Mar 1),"clamp"(last valid day of the month), or"skip"when a positivedaydoesn’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, orload(cdy, { priority }). A collection-level z-index. CalendarEvent.ordinal: the anniversary countoccurrenceYear - (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 derivationtoCollection()already used, now public).- Occurrence provenance:
CalendarEvent.movedFrom(the original date of a rescheduled occurrence) andgetSkipped({ from, to })(occurrences removed by anexceptionsskip / EXDATE) — render moved/ghost markers without re-reading configs.
The published
.cdyJSON Schema gainsorigin/keepOrdinals/onMissingDayand collection-levelpriority.
Minor Changes
Section titled “Minor Changes”-
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(); // afterNew
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:torange 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 unusedEventIndexwas removed from the hot path.count— end a series after N occurrences (RRULECOUNT). A new optional field on every event (builder:.times(n)), applied centrally so it works for core and plugin types alike. Needs astartDateorstartYear(builder:.starting(...)) to count from.Yearly interval.
constandnth-weekdayevents acceptinterval— every N years, phased from a requiredstartYear(builder:every(2, "years").on(date(6, 1)).starting(2025)).Extended
nth.nth-weekdaynow takes1–5from the start and-1–-5counted 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 theyear → Dateresolvers thatrelativeevents (builderfrom(anchor)) offset from.registerFormulastill works; the missing-anchor error now points atregisterAnchor.Fix.
search().first()and.exists()no longer permanently set the builder’slimitto 1.The
.cdyJSON Schema covers the new fields (count, yearlyinterval, extendednth). -
6359137:
EventConfig’s open (plugin) arm now carries the fullBaseEventProperties.Every event config — core or plugin — is guaranteed an
idandtitleat the type level, and the shared optional fields (priority,startDate,count,exceptions, …) are typed instead ofunknown. 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 withoutid/titlewill 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 ofbuild().New
calendaryjs/builderexport 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.cdyevents aloud. Returnsnullfor shapes with no builder form (plugin event types,formula, anchored-modenth-weekday,monthlywith astartMonthphase). Round-trip is tested: re-evaluating the sentence with the real builder reproduces the original config.
Patch Changes
Section titled “Patch Changes”- a04fddd: Point
homepageat the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs/licensepage and email contacts instead of links into the (private) GitHub repo.
Patch Changes
Section titled “Patch Changes”- dd3e09a: Ship
llms.txtinside 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 atcalendaryjs.dev/llms.txtonce the site is live.)
Minor Changes
Section titled “Minor Changes”- f801167: Ship a JSON Schema for the
.cdycollection format, for editor + AI tooling. The schema is bundled in the package (calendaryjs/schema/cdy.schema.json) and published athttps://calendaryjs.dev/schema/cdy.json. Collections now accept an optional$schemafield — add it to get autocomplete + on-the-spot validation in VS Code, and so an AI generating a.cdyself-corrects (it rejects the common mistakes, e.g.dateinstead ofday, an out-of-range month, an invalidnth). A compact LLM-oriented reference also ships at/llms.txt.
Patch Changes
Section titled “Patch Changes”- 89125f3: Docs: fix the Thanksgiving example in the README. The builder’s
nth()takes a 3-letter month name, so it’snth(4, "thursday", "nov")—"november"threw at runtime.
Minor Changes
Section titled “Minor Changes”-
f13dd06: License change: the core
calendaryjsis 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
dailyevents —every("day")/daily()/every(N, "days"). The missing 4th RRULE frequency;interval > 1is phased off a fixed anchor so “every N days” stays in step across the year boundary.- Nth weekday of every month —
every("month").on(nth(1, "monday"))(and thenth(n, weekday)2-arg form). Omitting the month repeats it monthly instead of needing 12 events. - Negative day-of-month —
day: -1= last day of the month (monthly(-1)),-2= second-to-last, formonthlyandconst.
Query
- Filter by
type,status, andsource:cal.search().type("weekly").status("confirmed").source("feed-x").getDays({ types })now applies its long-documented filter, and every computedCalendarEventcarries its originatingtype.
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 todaily/weekly/monthlytoo — previously those bounds were silently ignored on interval types.
Exports —
EventException,EventExceptions, andDailyEventtypes are now re-exported from the package root. -
8fa9303: Add
cal.toCollection()— export events as a portable collection, the inverse ofcal.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 throughload().JSON.stringifyit for a.cdydocument. Supports{ name, version, group }options.
Patch Changes
Section titled “Patch Changes”- f4eb2f9: Add a Socket security badge (plus npm version / zero-deps) to the README.
Patch Changes
Section titled “Patch Changes”- 38c23be: Fix the
calendaryjs/buildersubpath on the published package. It is now built as a tsup entry (dist/builder/) and mapped inpublishConfig.exports, soimport { every } from "calendaryjs/builder"works when installed from npm — previously only the root entry was built and exported, and the subpath 404’d.
Patch Changes
Section titled “Patch Changes”- 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-existentcalendary.extend).
Patch Changes
Section titled “Patch Changes”- 99ee489: Update package metadata for the new repository home (
vbilltran68/calendaryjs) and point npm-facing links at npm:homepageand the README cross-links resolve on npm, whilerepository/bugstrack the new GitHub repo. Also drop Day.js references from the docs.
Minor Changes
Section titled “Minor Changes”- f76e10e: Weekly events now accept multiple weekdays. Set
dayOfWeekto 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")andweekly("monday", "wednesday", "friday"). A single weekday is unchanged (dayOfWeek: number), so existing events keep working. Withinterval, all selected days share one reference week (ICSBYDAYsemantics).
- Initial public release — a lightweight, plugin-based calendar & recurrence engine.
calendaryjs-plugin-hijri
Section titled “calendaryjs-plugin-hijri”Patch Changes
Section titled “Patch Changes”- a04fddd: Point
homepageat the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs/licensepage and email contacts instead of links into the (private) GitHub repo.
Patch Changes
Section titled “Patch Changes”- f13dd06: License change: the core
calendaryjsis 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.
Patch Changes
Section titled “Patch Changes”- ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
Patch Changes
Section titled “Patch Changes”- 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-existentcalendary.extend).
Patch Changes
Section titled “Patch Changes”- 99ee489: Update package metadata for the new repository home (
vbilltran68/calendaryjs) and point npm-facing links at npm:homepageand the README cross-links resolve on npm, whilerepository/bugstrack the new GitHub repo. Also drop Day.js references from the docs.
- Initial public release — Islamic (Hijri) calendar plugin for calendaryjs.
calendaryjs-plugin-ics
Section titled “calendaryjs-plugin-ics”Patch Changes
Section titled “Patch Changes”- a04fddd: Point
homepageat the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs/licensepage and email contacts instead of links into the (private) GitHub repo.
Patch Changes
Section titled “Patch Changes”- f13dd06: License change: the core
calendaryjsis 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.
Patch Changes
Section titled “Patch Changes”- ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
Patch Changes
Section titled “Patch Changes”- 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-existentcalendary.extend).
Patch Changes
Section titled “Patch Changes”- 99ee489: Update package metadata for the new repository home (
vbilltran68/calendaryjs) and point npm-facing links at npm:homepageand the README cross-links resolve on npm, whilerepository/bugstrack the new GitHub repo. Also drop Day.js references from the docs.
calendaryjs-plugin-liturgical
Section titled “calendaryjs-plugin-liturgical”Patch Changes
Section titled “Patch Changes”- 976949c:
OffsetEvent.baseEventis now typed"easter" | (string & {})instead of plainstring— editors suggest"easter"(the only computed base) while the type stays open to any string. No runtime change.
Minor Changes
Section titled “Minor Changes”-
087dfb4: Split the offset date mechanism from the liturgical catalog semantics.
OffsetEvent<TMetadata>is now lean —baseEvent/offsetDayswith 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-LiturgicalMetadatashape moves to the newLiturgicalOffsetEvent<TExtra>, which the built-in presets/LITURGICAL_EVENTSuse.Note: if you typed a value as
OffsetEventexpecting requiredseason/rank/vestmentColor, switch it toLiturgicalOffsetEvent.
Patch Changes
Section titled “Patch Changes”-
6359137:
EventConfig’s open (plugin) arm now carries the fullBaseEventProperties.Every event config — core or plugin — is guaranteed an
idandtitleat the type level, and the shared optional fields (priority,startDate,count,exceptions, …) are typed instead ofunknown. 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 withoutid/titlewill 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.
Patch Changes
Section titled “Patch Changes”- a04fddd: Point
homepageat the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs/licensepage and email contacts instead of links into the (private) GitHub repo.
Patch Changes
Section titled “Patch Changes”- f13dd06: License change: the core
calendaryjsis 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.
Patch Changes
Section titled “Patch Changes”- ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
Patch Changes
Section titled “Patch Changes”- 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-existentcalendary.extend).
Patch Changes
Section titled “Patch Changes”- 99ee489: Update package metadata for the new repository home (
vbilltran68/calendaryjs) and point npm-facing links at npm:homepageand the README cross-links resolve on npm, whilerepository/bugstrack the new GitHub repo. Also drop Day.js references from the docs.
- Initial public release — Catholic liturgical calendar plugin (Easter computus + offset feasts).
calendaryjs-plugin-lunar
Section titled “calendaryjs-plugin-lunar”Minor Changes
Section titled “Minor Changes”-
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
calendarfield (builder:lunar.date(2, 12, { calendar: "vietnamese" })), which wins over the instance default — so a shared.cdycollection is self-describing and renders the same dates for every consumer.solarToLunar/lunarToSolar/isValidLunarDate/lunar.fromSolartake 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).
Minor Changes
Section titled “Minor Changes”- 5222487: Add
isLeapMonthto lunar events — and{ leap: true }to thelunar.date()builder selector — to target a leap month (闰月). In years without that leap month the event falls back to the regular month, matching Temporal’smonthCode. Newlunar.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).
Patch Changes
Section titled “Patch Changes”- a04fddd: Point
homepageat the now-live docs site (calendaryjs.dev) and republish the READMEs with working license/contact links — the docs/licensepage and email contacts instead of links into the (private) GitHub repo.
Patch Changes
Section titled “Patch Changes”- f13dd06: License change: the core
calendaryjsis 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.
Patch Changes
Section titled “Patch Changes”- ef9f60c: Add npm version, Socket security, and zero-deps badges to each plugin README.
Patch Changes
Section titled “Patch Changes”- 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-existentcalendary.extend).
Patch Changes
Section titled “Patch Changes”- 99ee489: Update package metadata for the new repository home (
vbilltran68/calendaryjs) and point npm-facing links at npm:homepageand the README cross-links resolve on npm, whilerepository/bugstrack the new GitHub repo. Also drop Day.js references from the docs.
- Initial public release — lunar (lunisolar) calendar plugin for calendaryjs.