commit 96dd9c5688c2436313c8ea980d42326dda4ff06f Author: Aric Camarata Date: Wed Feb 25 14:15:07 2026 -0500 feat: initial release of dayjs-hijri-plus v1.0.0 Day.js plugin adding Hijri calendar support via hijri-core. Adds toHijri(), fromHijri(), hijriYear/Month/Day(), isValidHijri(), and formatHijri() to all Day.js instances. Supports UAQ and FCNA calendars via ConversionOptions. Format token escaping wraps substituted values in Day.js bracket syntax to prevent re-interpretation as format tokens. 14 ESM + 8 CJS tests passing. Dual CJS/ESM build. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..89e5874 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{ts,js,mjs,cjs,json,yaml,yml,md}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1d0f046 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test (Node ${{ matrix.node }}) + runs-on: ubuntu-latest + strategy: + matrix: + node: [20, 22, 24] + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run build + - run: node test.mjs + - run: node test-cjs.cjs + + typecheck: + name: Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run typecheck + + pack-check: + name: Pack check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run build + - name: Verify pack contents + run: | + npm pack --dry-run 2>&1 | tee pack-output.txt + grep "dist/index.cjs" pack-output.txt + grep "dist/index.mjs" pack-output.txt + grep "dist/index.d.ts" pack-output.txt + grep "dist/index.d.mts" pack-output.txt + grep "README.md" pack-output.txt + grep "CHANGELOG.md" pack-output.txt + grep "LICENSE" pack-output.txt diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml new file mode 100644 index 0000000..17253f7 --- /dev/null +++ b/.github/workflows/wiki-sync.yml @@ -0,0 +1,36 @@ +name: Wiki Sync + +on: + push: + branches: [main] + paths: + - '.wiki/**' + +permissions: + contents: write + +jobs: + sync: + name: Sync .wiki/ to GitHub Wiki + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Push wiki pages + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }}.wiki + path: wiki-repo + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Copy wiki files + run: cp .wiki/*.md wiki-repo/ + + - name: Commit and push + working-directory: wiki-repo + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --quiet || git commit -m "Sync wiki from .wiki/ [skip ci]" + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7860879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.tgz +*.log +.DS_Store +.claude/ +.env +.env.* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..391eb15 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-import-method=hardlink diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.wiki/API-Reference.md b/.wiki/API-Reference.md new file mode 100644 index 0000000..1a21982 --- /dev/null +++ b/.wiki/API-Reference.md @@ -0,0 +1,205 @@ +# API Reference + +## Setup + +```ts +import dayjs from 'dayjs'; +import hijriPlugin from 'dayjs-hijri-plus'; + +dayjs.extend(hijriPlugin); +``` + +Call `dayjs.extend` once, globally. After that, every Day.js instance has the plugin methods. + +--- + +## Instance Methods + +### `.toHijri(opts?)` + +Convert the Day.js date to a Hijri date. + +**Signature:** +```ts +toHijri(opts?: ConversionOptions): HijriDate | null +``` + +**Parameters:** + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `opts.calendar` | `string` | `'uaq'` | Calendar engine id. Built-ins: `'uaq'`, `'fcna'` | + +**Returns:** `{ hy: number, hm: number, hd: number }` or `null` if the date is outside the table range. + +```ts +dayjs('2023-03-23').toHijri(); +// => { hy: 1444, hm: 9, hd: 1 } + +dayjs('2023-03-23').toHijri({ calendar: 'fcna' }); +// => { hy: 1444, hm: 9, hd: 2 } +``` + +--- + +### `.isValidHijri(opts?)` + +Check whether the date has a valid Hijri representation in the supported range. + +**Signature:** +```ts +isValidHijri(opts?: ConversionOptions): boolean +``` + +Returns `false` for dates outside the coverage range, `true` otherwise. + +--- + +### `.hijriYear(opts?)` + +**Signature:** +```ts +hijriYear(opts?: ConversionOptions): number | null +``` + +Returns the Hijri year, or `null` if out of range. + +--- + +### `.hijriMonth(opts?)` + +**Signature:** +```ts +hijriMonth(opts?: ConversionOptions): number | null +``` + +Returns the Hijri month (1-12), or `null` if out of range. + +--- + +### `.hijriDay(opts?)` + +**Signature:** +```ts +hijriDay(opts?: ConversionOptions): number | null +``` + +Returns the Hijri day (1-30), or `null` if out of range. + +--- + +### `.formatHijri(formatStr, opts?)` + +Format the date using a mix of Hijri-specific tokens and standard Day.js tokens. + +**Signature:** +```ts +formatHijri(formatStr: string, opts?: ConversionOptions): string +``` + +**Parameters:** + +| Name | Type | Description | +| --- | --- | --- | +| `formatStr` | `string` | Format string containing Hijri tokens, Day.js tokens, or both | +| `opts` | `ConversionOptions` | Optional calendar selection | + +Returns an empty string if the date is outside the supported range. + +**Hijri tokens:** + +| Token | Example | Description | +| --- | --- | --- | +| `iYYYY` | `1444` | 4-digit Hijri year | +| `iYY` | `44` | 2-digit Hijri year | +| `iMMMM` | `Ramadan` | Full month name | +| `iMMM` | `Ramadan` | Medium month name | +| `iMM` | `09` | Zero-padded month number | +| `iM` | `9` | Month number | +| `iDD` | `01` | Zero-padded day | +| `iD` | `1` | Day number | +| `iEEEE` | `Yawm al-Khamis` | Full weekday name | +| `iEEE` | `Kham` | Short weekday name | +| `iE` | `5` | Weekday number (1=Sun ... 7=Sat) | +| `ioooo` | `AH` | Era | +| `iooo` | `AH` | Era (same as ioooo) | + +Standard Day.js tokens pass through to `.format()` after Hijri token substitution. + +```ts +dayjs('2023-03-23').formatHijri('iYYYY-iMM-iDD'); +// => '1444-09-01' + +dayjs('2023-03-23').formatHijri('iD iMMMM iYYYY [at] HH:mm'); +// => '1 Ramadan 1444 at 00:00' + +dayjs('2023-03-23').formatHijri('iYYYY YYYY'); +// => '1444 2023' +``` + +--- + +## Static Methods + +### `dayjs.fromHijri(hy, hm, hd, opts?)` + +Construct a Day.js instance from a Hijri date. + +**Signature:** +```ts +dayjs.fromHijri( + hy: number, + hm: number, + hd: number, + opts?: ConversionOptions, +): dayjs.Dayjs +``` + +**Parameters:** + +| Name | Type | Description | +| --- | --- | --- | +| `hy` | `number` | Hijri year | +| `hm` | `number` | Hijri month (1-12) | +| `hd` | `number` | Hijri day (1-30) | +| `opts.calendar` | `string` | Calendar engine id (default: `'uaq'`) | + +**Throws:** `Error` if the Hijri date is invalid or outside the table range. + +```ts +dayjs.fromHijri(1444, 9, 1).format('YYYY-MM-DD'); +// => '2023-03-23' + +dayjs.fromHijri(1444, 9, 1, { calendar: 'fcna' }).format('YYYY-MM-DD'); +// => '2023-03-22' +``` + +--- + +## Type Exports + +```ts +import type { + HijriDate, // { hy: number, hm: number, hd: number } + ConversionOptions, // { calendar?: string } + CalendarSystem, // string alias for calendar ids +} from 'dayjs-hijri-plus'; +``` + +--- + +## Registry Exports + +These re-export from hijri-core, so consumers can register custom calendar engines without adding hijri-core as a direct dependency: + +```ts +import { registerCalendar, getCalendar, listCalendars } from 'dayjs-hijri-plus'; +import type { CalendarEngine } from 'dayjs-hijri-plus'; + +registerCalendar('my-cal', myEngine); +listCalendars(); // => ['uaq', 'fcna', 'my-cal'] +``` + +--- + +[Home](Home) | [Architecture](Architecture) diff --git a/.wiki/Architecture.md b/.wiki/Architecture.md new file mode 100644 index 0000000..9ab505e --- /dev/null +++ b/.wiki/Architecture.md @@ -0,0 +1,99 @@ +# Architecture + +## Design Philosophy + +dayjs-hijri-plus contains no Hijri calendar arithmetic. Every conversion delegates to [hijri-core](https://github.com/acamarata/hijri-core), which provides a pluggable engine registry with UAQ and FCNA built in. + +This separation is deliberate. Calendar algorithms are complex, have known edge cases, and require dedicated testing. Keeping them in hijri-core means both this plugin and future adapters (for Temporal, date-fns, etc.) share a single, well-tested core. + +## Plugin Structure + +``` +src/ + index.ts Plugin entry — registers methods on dayjsClass and dayjsFactory + types.ts Type definitions and module augmentation for dayjs +``` + +The plugin follows the standard Day.js `PluginFunc` signature: + +```ts +const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => { ... }; +``` + +- `dayjsClass.prototype.*` — instance methods (`.toHijri`, `.formatHijri`, etc.) +- `(dayjsFactory as any).fromHijri` — static method added to the factory function + +## Peer Dependencies + +Both `dayjs` and `hijri-core` are peer dependencies. This means: + +1. The host application controls which version of `dayjs` is used. No version conflict possible. +2. The host application controls which version of `hijri-core` is used. If hijri-core ships updated tables covering new years, the plugin benefits automatically. +3. The plugin itself has zero runtime dependencies in `node_modules` — only peer resolutions. + +## Format Token Resolution + +`formatHijri` works in two passes: + +**Pass 1:** Replace Hijri tokens using a single regex sweep over the format string. + +```ts +const HIJRI_TOKEN_RE = /iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g; +``` + +Tokens are listed longest-first in the alternation. This prevents `iYY` from matching before `iYYYY`, and `iMM` from matching before `iMMMM`. The regex engine tries alternatives left-to-right at each position, so ordering is the only safeguard needed. + +**Pass 2:** The modified string is passed to `this.format(result)`. Day.js resolves all remaining tokens (YYYY, MM, DD, HH, mm, ss, etc.) and square-bracket escapes (`[literal]`). + +This means Hijri tokens and Gregorian tokens can coexist in the same format string. For example, `'iYYYY YYYY'` produces `'1444 2023'`. + +## Weekday Alignment + +Day.js `.day()` returns `0` for Sunday through `6` for Saturday — the same convention as `Date.prototype.getDay()`. + +The weekday arrays exported by hijri-core (`hwLong`, `hwShort`, `hwNumeric`) use the same index layout: index `0` = Sunday, index `6` = Saturday. So `hwLong[this.day()]` always yields the correct weekday name with no offset arithmetic. + +## fromHijri Error Handling + +`dayjs.fromHijri` calls `toGregorian` from hijri-core. If the Hijri date is invalid or outside the table range, `toGregorian` returns `null`. The plugin converts that into a thrown `Error` with the specific Hijri components included in the message, so callers get a useful diagnostic rather than a null-dereference downstream. + +## Calendar Extension + +The registry is global within a process. Registering a custom calendar once makes it available to all plugin method calls: + +```ts +import { registerCalendar } from 'dayjs-hijri-plus'; + +registerCalendar('tabular', tabularEngine); + +dayjs('2023-03-23').toHijri({ calendar: 'tabular' }); +``` + +Custom engines must implement the `CalendarEngine` interface from hijri-core: + +```ts +interface CalendarEngine { + readonly id: string; + toHijri(date: Date): HijriDate | null; + toGregorian(hy: number, hm: number, hd: number): Date | null; + isValid(hy: number, hm: number, hd: number): boolean; + daysInMonth(hy: number, hm: number): number; +} +``` + +## Build + +The package ships a dual CJS/ESM build via tsup. Both `dayjs` and `hijri-core` are marked as `external`, so they are never bundled — consumers provide them via peer dependency resolution. + +Output: + +| File | Format | +| --- | --- | +| `dist/index.cjs` | CommonJS (Node `require`) | +| `dist/index.mjs` | ESM (`import`) | +| `dist/index.d.ts` | TypeScript declarations for CJS | +| `dist/index.d.mts` | TypeScript declarations for ESM | + +--- + +[Home](Home) | [API Reference](API-Reference) diff --git a/.wiki/Home.md b/.wiki/Home.md new file mode 100644 index 0000000..4872b83 --- /dev/null +++ b/.wiki/Home.md @@ -0,0 +1,36 @@ +# dayjs-hijri-plus + +A Day.js plugin for Hijri calendar conversion and formatting. All calendar logic is delegated to [hijri-core](https://github.com/acamarata/hijri-core), making this package a thin, well-typed adapter with no calendar arithmetic of its own. + +## Install + +```sh +pnpm add dayjs dayjs-hijri-plus hijri-core +``` + +## Quick Usage + +```ts +import dayjs from 'dayjs'; +import hijriPlugin from 'dayjs-hijri-plus'; + +dayjs.extend(hijriPlugin); + +dayjs('2023-03-23').toHijri(); +// => { hy: 1444, hm: 9, hd: 1 } + +dayjs('2023-03-23').formatHijri('iD iMMMM iYYYY'); +// => '1 Ramadan 1444' + +dayjs.fromHijri(1444, 10, 1).format('YYYY-MM-DD'); +// => '2023-04-21' +``` + +## Contents + +- [API Reference](API-Reference) — all methods, parameters, return types +- [Architecture](Architecture) — design decisions, delegation model, format token resolution + +--- + +Part of the [acamarata](https://github.com/acamarata) JavaScript library collection. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d01f558 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [1.0.0] - 2026-02-25 + +### Added + +- Day.js plugin with `.toHijri()`, `.fromHijri()`, `.hijriYear()`, `.hijriMonth()`, `.hijriDay()`, `.isValidHijri()`, and `.formatHijri()` methods +- Umm al-Qura (UAQ) calendar support via hijri-core +- FCNA/ISNA calendar support via hijri-core +- Full TypeScript definitions including module augmentation for Day.js types +- Dual CJS/ESM build with separate type declaration files +- Re-exports of `registerCalendar`, `getCalendar`, and `listCalendars` from hijri-core for custom calendar registration diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f8c021c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Aric Camarata + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..71837a2 --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# dayjs-hijri-plus + +[![npm version](https://img.shields.io/npm/v/dayjs-hijri-plus)](https://www.npmjs.com/package/dayjs-hijri-plus) +[![CI](https://github.com/acamarata/dayjs-hijri-plus/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/dayjs-hijri-plus/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +A Day.js plugin that adds Hijri calendar support. Converts Gregorian dates to and from Hijri, provides Hijri-aware formatting, and delegates all calendar logic to [hijri-core](https://github.com/acamarata/hijri-core) — keeping this package thin and testable. + +Supports Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. Custom calendar engines can be registered at runtime. + +## Installation + +```sh +pnpm add dayjs dayjs-hijri-plus hijri-core +``` + +Both `dayjs` and `hijri-core` are peer dependencies and must be installed alongside this plugin. + +## Quick Start + +```ts +import dayjs from 'dayjs'; +import hijriPlugin from 'dayjs-hijri-plus'; + +dayjs.extend(hijriPlugin); + +// Convert a Gregorian date to Hijri +const d = dayjs(new Date(2023, 2, 23)); +const hijri = d.toHijri(); +// => { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH) + +// Format using Hijri tokens mixed with standard Day.js tokens +d.formatHijri('iYYYY-iMM-iDD'); // => '1444-09-01' +d.formatHijri('iD iMMMM iYYYY'); // => '1 Ramadan 1444' +d.formatHijri('iD iMMMM iYYYY [at] HH:mm'); // => '1 Ramadan 1444 at 00:00' + +// Individual Hijri components +d.hijriYear(); // => 1444 +d.hijriMonth(); // => 9 +d.hijriDay(); // => 1 + +// Construct a Day.js instance from a Hijri date +const eid = dayjs.fromHijri(1444, 10, 1); +eid.format('YYYY-MM-DD'); // => '2023-04-21' + +// FCNA/ISNA calendar variant +d.toHijri({ calendar: 'fcna' }); // => { hy: 1444, hm: 9, hd: 2 } (varies by month) +``` + +## API + +### dayjs.extend(hijriPlugin) + +Register the plugin with your Day.js instance. Call once before using any plugin methods. + +### Instance Methods + +#### `.toHijri(opts?)` + +Convert the Day.js date to a Hijri date object. + +| Parameter | Type | Description | +| --- | --- | --- | +| `opts` | `ConversionOptions` | Optional. `{ calendar: 'uaq' \| 'fcna' \| string }` | + +Returns `HijriDate | null`. Returns `null` if the date is outside the supported range (approximately 1900-2077 CE for UAQ). + +```ts +dayjs('2023-03-23').toHijri(); +// => { hy: 1444, hm: 9, hd: 1 } +``` + +#### `.isValidHijri(opts?)` + +Check whether the date maps to a valid Hijri date in the supported range. + +Returns `boolean`. + +#### `.hijriYear(opts?)` + +Returns the Hijri year as a `number`, or `null` if out of range. + +#### `.hijriMonth(opts?)` + +Returns the Hijri month (1-12) as a `number`, or `null` if out of range. + +#### `.hijriDay(opts?)` + +Returns the Hijri day (1-30) as a `number`, or `null` if out of range. + +#### `.formatHijri(formatStr, opts?)` + +Format the date using a mix of Hijri tokens and standard Day.js tokens. + +| Parameter | Type | Description | +| --- | --- | --- | +| `formatStr` | `string` | Format string — see token table below | +| `opts` | `ConversionOptions` | Optional calendar selection | + +Returns `string`. Returns an empty string if the date is out of range. + +Hijri tokens are replaced first. The resulting string is then passed to Day.js `.format()`, so all standard tokens (YYYY, MM, DD, HH, mm, ss, etc.) resolve normally. + +### Static Methods + +#### `dayjs.fromHijri(hy, hm, hd, opts?)` + +Construct a Day.js instance from a Hijri date. + +| Parameter | Type | Description | +| --- | --- | --- | +| `hy` | `number` | Hijri year | +| `hm` | `number` | Hijri month (1-12) | +| `hd` | `number` | Hijri day (1-30) | +| `opts` | `ConversionOptions` | Optional calendar selection | + +Returns a `dayjs.Dayjs` instance. Throws `Error` if the Hijri date is invalid or outside the table range. + +## Format Tokens + +All Hijri-specific tokens use the `i` prefix. + +| Token | Example | Description | +| --- | --- | --- | +| `iYYYY` | `1444` | 4-digit Hijri year | +| `iYY` | `44` | 2-digit Hijri year | +| `iMMMM` | `Ramadan` | Full Hijri month name | +| `iMMM` | `Ramadan` | Medium Hijri month name | +| `iMM` | `09` | Zero-padded Hijri month number | +| `iM` | `9` | Hijri month number | +| `iDD` | `01` | Zero-padded Hijri day | +| `iD` | `1` | Hijri day number | +| `iEEEE` | `Yawm al-Khamis` | Full weekday name | +| `iEEE` | `Kham` | Short weekday name | +| `iE` | `5` | Weekday number (1=Sunday ... 7=Saturday) | +| `ioooo` | `AH` | Era (Anno Hegirae) | +| `iooo` | `AH` | Era (short form, same as ioooo) | + +Standard Day.js tokens pass through untouched. Square-bracket escaping (`[literal text]`) also works as expected. + +## Calendar Systems + +Two calendars ship with hijri-core: + +- **`uaq`** (default) — Umm al-Qura, the official calendar of Saudi Arabia. Table-based, covers approximately 1318-1500 AH (1900-2077 CE). +- **`fcna`** — Fiqh Council of North America calendar. Uses an astronomical calculation with fixed criteria, independent of moon sighting. + +Select a calendar by passing `{ calendar: 'fcna' }` to any method. The default is `'uaq'` when no option is provided. + +Custom calendar engines can be registered: + +```ts +import { registerCalendar } from 'dayjs-hijri-plus'; +import type { CalendarEngine } from 'dayjs-hijri-plus'; + +const myEngine: CalendarEngine = { ... }; +registerCalendar('my-calendar', myEngine); + +dayjs().toHijri({ calendar: 'my-calendar' }); +``` + +See the [hijri-core CalendarEngine interface](https://github.com/acamarata/hijri-core) for the full contract. + +## TypeScript + +Full TypeScript support is included. The plugin augments the Day.js module to add types for all instance and static methods. + +```ts +import type { HijriDate, ConversionOptions } from 'dayjs-hijri-plus'; + +const h: HijriDate = dayjs().toHijri()!; +const opts: ConversionOptions = { calendar: 'fcna' }; +``` + +No `@types` package is needed. + +## Documentation + +Full API reference, architecture notes, and calendar system comparisons are on the [GitHub Wiki](https://github.com/acamarata/dayjs-hijri-plus/wiki). + +## Related + +- [hijri-core](https://github.com/acamarata/hijri-core) — the zero-dependency Hijri calendar engine this plugin wraps +- [luxon-hijri](https://github.com/acamarata/luxon-hijri) — the same Hijri conversion for Luxon users +- [pray-calc](https://github.com/acamarata/pray-calc) — Islamic prayer time calculation +- [nrel-spa](https://github.com/acamarata/nrel-spa) — NREL Solar Position Algorithm in pure JavaScript + +## License + +MIT. Copyright (c) 2026 Aric Camarata. diff --git a/package.json b/package.json new file mode 100644 index 0000000..214a855 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "dayjs-hijri-plus", + "version": "1.0.0", + "description": "Day.js plugin for Hijri calendar conversion and formatting. Supports Umm al-Qura and FCNA calendars via hijri-core.", + "author": "Aric Camarata", + "license": "MIT", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" }, + "require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" } + } + }, + "sideEffects": false, + "files": [ + "dist/index.cjs", + "dist/index.mjs", + "dist/index.d.ts", + "dist/index.d.mts", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "engines": { "node": ">=20" }, + "packageManager": "pnpm@10.30.1", + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit", + "pretest": "tsup", + "test": "node test.mjs && node test-cjs.cjs", + "prepublishOnly": "tsup" + }, + "keywords": [ + "dayjs", + "plugin", + "hijri", + "islamic", + "calendar", + "umm-al-qura", + "fcna", + "gregorian", + "converter", + "typescript" + ], + "peerDependencies": { + "dayjs": "^1.0.0", + "hijri-core": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "dayjs": "^1.11.0", + "hijri-core": "file:../hijri-core", + "tsup": "^8.0.0", + "typescript": "^5.5.0" + }, + "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }, + "repository": { "type": "git", "url": "git+https://github.com/acamarata/dayjs-hijri-plus.git" }, + "homepage": "https://github.com/acamarata/dayjs-hijri-plus#readme", + "bugs": { "url": "https://github.com/acamarata/dayjs-hijri-plus/issues" } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ec56175 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,942 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.11 + dayjs: + specifier: ^1.11.0 + version: 1.11.19 + hijri-core: + specifier: file:../hijri-core + version: file:../hijri-core + tsup: + specifier: ^8.0.0 + version: 8.5.1(typescript@5.9.3) + typescript: + specifier: ^5.5.0 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hijri-core@file:../hijri-core: + resolution: {directory: ../hijri-core, type: directory} + engines: {node: '>=20'} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + + acorn@8.16.0: {} + + any-promise@1.3.0: {} + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + dayjs@1.11.19: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.59.0 + + fsevents@2.3.3: + optional: true + + hijri-core@file:../hijri-core: {} + + joycon@3.1.1: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + object-assign@4.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + source-map@0.7.6: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@6.21.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2a707f6 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,160 @@ +import type { PluginFunc } from 'dayjs'; +import { + toHijri, + toGregorian, + hmLong, + hmMedium, + hwLong, + hwShort, + hwNumeric, +} from 'hijri-core'; +import type { ConversionOptions, HijriDate } from './types'; + +// Augment Day.js to expose plugin methods on the instance type. +declare module 'dayjs' { + interface Dayjs { + /** Convert to a Hijri date. Returns null if outside the supported range. */ + toHijri(opts?: ConversionOptions): HijriDate | null; + + /** Check whether the date maps to a valid Hijri date in the supported range. */ + isValidHijri(opts?: ConversionOptions): boolean; + + /** Hijri year component, or null if out of range. */ + hijriYear(opts?: ConversionOptions): number | null; + + /** Hijri month component (1-12), or null if out of range. */ + hijriMonth(opts?: ConversionOptions): number | null; + + /** Hijri day component (1-30), or null if out of range. */ + hijriDay(opts?: ConversionOptions): number | null; + + /** + * Format the date using Hijri tokens (i-prefixed) and standard Day.js tokens. + * Returns an empty string if the date is outside the supported range. + */ + formatHijri(formatStr: string, opts?: ConversionOptions): string; + } +} + +// Augment the dayjs factory to expose the fromHijri static method. +declare module 'dayjs' { + interface IStatic { + /** + * Construct a Day.js instance from a Hijri date. + * Throws if the Hijri date is invalid or outside the supported range. + */ + fromHijri(hy: number, hm: number, hd: number, opts?: ConversionOptions): import('dayjs').Dayjs; + } +} + +// Hijri-specific format tokens, ordered longest-first to prevent partial matches. +// After replacement, the remaining string is passed to Day.js .format() for +// standard tokens (YYYY, MM, DD, HH, mm, ss, etc.). +const HIJRI_TOKEN_RE = /iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g; + +/** + * Wrap a plain string value in Day.js bracket-escape syntax so that + * `.format()` treats every character as a literal. + * + * Day.js uses `[...]` for literal text. A `]` inside such a section would + * close it prematurely, so we split on `]` and re-join with `][` (which + * closes the current literal section, outputs a raw `]` — Day.js passes + * unrecognised characters through untouched — then opens a new one). + */ +function lit(value: string): string { + return '[' + value.split(']').join('][') + ']'; +} + +const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => { + // ------------------------------------------------------------------ // + // Instance methods // + // ------------------------------------------------------------------ // + + dayjsClass.prototype.toHijri = function (opts?: ConversionOptions): HijriDate | null { + return toHijri(this.toDate(), opts); + }; + + dayjsClass.prototype.isValidHijri = function (opts?: ConversionOptions): boolean { + return this.toHijri(opts) !== null; + }; + + dayjsClass.prototype.hijriYear = function (opts?: ConversionOptions): number | null { + return this.toHijri(opts)?.hy ?? null; + }; + + dayjsClass.prototype.hijriMonth = function (opts?: ConversionOptions): number | null { + return this.toHijri(opts)?.hm ?? null; + }; + + dayjsClass.prototype.hijriDay = function (opts?: ConversionOptions): number | null { + return this.toHijri(opts)?.hd ?? null; + }; + + dayjsClass.prototype.formatHijri = function ( + formatStr: string, + opts?: ConversionOptions, + ): string { + const hijri = this.toHijri(opts); + if (!hijri) return ''; + + // Day.js .day() returns 0 (Sunday) ... 6 (Saturday), matching the index + // layout of hwLong, hwShort, and hwNumeric from hijri-core. + const dow = this.day(); + + const replaced = formatStr.replace(HIJRI_TOKEN_RE, (token) => { + switch (token) { + case 'iYYYY': return lit(String(hijri.hy).padStart(4, '0')); + case 'iYY': return lit(String(hijri.hy % 100).padStart(2, '0')); + case 'iMMMM': return lit(hmLong[hijri.hm - 1]); + case 'iMMM': return lit(hmMedium[hijri.hm - 1]); + case 'iMM': return lit(String(hijri.hm).padStart(2, '0')); + case 'iM': return lit(String(hijri.hm)); + case 'iDD': return lit(String(hijri.hd).padStart(2, '0')); + case 'iD': return lit(String(hijri.hd)); + case 'iEEEE': return lit(hwLong[dow]); + case 'iEEE': return lit(hwShort[dow]); + case 'iE': return lit(String(hwNumeric[dow])); + case 'ioooo': + case 'iooo': return lit('AH'); + default: return token; + } + }); + + // Pass the processed string to Day.js .format() so standard tokens + // (YYYY, MM, DD, HH, mm, ss, etc.) resolve correctly. Hijri values are + // already wrapped in bracket-escaped literals and pass through untouched. + return this.format(replaced); + }; + + // ------------------------------------------------------------------ // + // Static method: dayjs.fromHijri(hy, hm, hd, opts?) // + // ------------------------------------------------------------------ // + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (dayjsFactory as any).fromHijri = ( + hy: number, + hm: number, + hd: number, + opts?: ConversionOptions, + ) => { + let greg: Date | null; + try { + greg = toGregorian(hy, hm, hd, opts); + } catch { + throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`); + } + if (!greg) { + throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`); + } + return dayjsFactory(greg); + }; +}; + +export default plugin; + +// Re-export hijri-core types for consumers who import from dayjs-hijri-plus. +export type { HijriDate, ConversionOptions, CalendarSystem } from './types'; + +// Re-export the registry API so callers can register custom calendar engines +// without adding hijri-core as a direct dependency. +export { registerCalendar, getCalendar, listCalendars } from 'hijri-core'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..289b2a1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,13 @@ +import type { HijriDate, ConversionOptions } from 'hijri-core'; +export type { HijriDate, ConversionOptions }; + +/** A registered calendar identifier. The built-in values are 'uaq' and 'fcna'. */ +export type CalendarSystem = string; + +/** + * Options passed to plugin methods. Inherits `calendar` from ConversionOptions + * so callers can switch between 'uaq' (default) and 'fcna'. + */ +export interface HijriPluginOptions extends ConversionOptions { + // calendar?: string (inherited — 'uaq' | 'fcna' | any registered calendar id) +} diff --git a/test-cjs.cjs b/test-cjs.cjs new file mode 100644 index 0000000..d3bbbe5 --- /dev/null +++ b/test-cjs.cjs @@ -0,0 +1,68 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const dayjs = require('dayjs'); +const { default: plugin } = require('./dist/index.cjs'); + +dayjs.extend(plugin); + +let passed = 0; +let total = 0; + +function test(name, fn) { + total++; + try { + fn(); + console.log(`[${name}]... PASS`); + passed++; + } catch (err) { + console.error(`[${name}]... FAIL: ${err.message}`); + process.exit(1); + } +} + +const D_RAMADAN_1444 = new Date(2023, 2, 23, 12); +const D_MUHARRAM_1446 = new Date(2024, 6, 7, 12); + +test('plugin registers (CJS)', () => { + const d = dayjs(D_RAMADAN_1444); + assert.equal(typeof d.toHijri, 'function'); + assert.equal(typeof d.formatHijri, 'function'); + assert.equal(typeof dayjs.fromHijri, 'function'); +}); + +test('toHijri (CJS): 2023-03-23 -> 1 Ramadan 1444', () => { + const h = dayjs(D_RAMADAN_1444).toHijri(); + assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 }); +}); + +test('toHijri (CJS): 2024-07-07 -> 1 Muharram 1446', () => { + const h = dayjs(D_MUHARRAM_1446).toHijri(); + assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 }); +}); + +test('fromHijri (CJS): 1444/9/1 -> 2023-03-23 (UTC)', () => { + const d = dayjs.fromHijri(1444, 9, 1); + const iso = d.toDate().toISOString(); + assert.ok(iso.startsWith('2023-03-23'), `Expected 2023-03-23, got ${iso}`); +}); + +test('formatHijri (CJS): iYYYY-iMM-iDD', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY-iMM-iDD'); + assert.equal(result, '1444-09-01'); +}); + +test('formatHijri (CJS): iMMMM -> Ramadan', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('iMMMM'); + assert.equal(result, 'Ramadan'); +}); + +test('isValidHijri (CJS): true for in-range date', () => { + assert.equal(dayjs(D_RAMADAN_1444).isValidHijri(), true); +}); + +test('fromHijri (CJS): throws for out-of-range date', () => { + assert.throws(() => dayjs.fromHijri(1301, 1, 1), /Invalid or out-of-range/); +}); + +console.log(`\n${passed}/${total} tests passed`); diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..cbb8edb --- /dev/null +++ b/test.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import dayjs from 'dayjs'; +import plugin from './dist/index.mjs'; + +dayjs.extend(plugin); + +let passed = 0; +let total = 0; + +function test(name, fn) { + total++; + try { + fn(); + console.log(`[${name}]... PASS`); + passed++; + } catch (err) { + console.error(`[${name}]... FAIL: ${err.message}`); + process.exit(1); + } +} + +// Use noon to avoid UTC midnight boundary issues across timezones. +const D_RAMADAN_1444 = new Date(2023, 2, 23, 12); // 1 Ramadan 1444 +const D_MUHARRAM_1446 = new Date(2024, 6, 7, 12); // 1 Muharram 1446 + +test('plugin registers on dayjs', () => { + const d = dayjs(D_RAMADAN_1444); + assert.equal(typeof d.toHijri, 'function'); + assert.equal(typeof d.formatHijri, 'function'); + assert.equal(typeof d.isValidHijri, 'function'); + assert.equal(typeof d.hijriYear, 'function'); + assert.equal(typeof d.hijriMonth, 'function'); + assert.equal(typeof d.hijriDay, 'function'); + assert.equal(typeof dayjs.fromHijri, 'function'); +}); + +test('toHijri: 2023-03-23 -> 1 Ramadan 1444', () => { + const h = dayjs(D_RAMADAN_1444).toHijri(); + assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 }); +}); + +test('toHijri: 2024-07-07 -> 1 Muharram 1446', () => { + const h = dayjs(D_MUHARRAM_1446).toHijri(); + assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 }); +}); + +test('fromHijri: 1444/9/1 -> 2023-03-23 (UTC)', () => { + const d = dayjs.fromHijri(1444, 9, 1); + // toGregorian returns midnight UTC; compare using UTC accessors to be timezone-safe. + const iso = d.toDate().toISOString(); + assert.ok(iso.startsWith('2023-03-23'), `Expected 2023-03-23, got ${iso}`); +}); + +test('fromHijri: 1446/1/1 -> 2024-07-07 (UTC)', () => { + const d = dayjs.fromHijri(1446, 1, 1); + const iso = d.toDate().toISOString(); + assert.ok(iso.startsWith('2024-07-07'), `Expected 2024-07-07, got ${iso}`); +}); + +test('hijriYear/hijriMonth/hijriDay accessors on 1 Ramadan 1444', () => { + const d = dayjs(D_RAMADAN_1444); + assert.equal(d.hijriYear(), 1444); + assert.equal(d.hijriMonth(), 9); + assert.equal(d.hijriDay(), 1); +}); + +test('formatHijri: iYYYY-iMM-iDD on 1 Ramadan 1444', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY-iMM-iDD'); + assert.equal(result, '1444-09-01'); +}); + +test('formatHijri: iMMMM -> Ramadan', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('iMMMM'); + assert.equal(result, 'Ramadan'); +}); + +test('formatHijri: iEEEE on 2023-03-23 (Thursday)', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('iEEEE'); + // 2023-03-23 is a Thursday; hwLong[4] = 'Yawm al-Khamis' + assert.equal(result, 'Yawm al-Khamis'); +}); + +test('formatHijri: ioooo -> AH', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('ioooo'); + assert.equal(result, 'AH'); +}); + +test('FCNA calendar: toHijri returns a valid HijriDate', () => { + const h = dayjs(D_RAMADAN_1444).toHijri({ calendar: 'fcna' }); + assert.notEqual(h, null); + assert.equal(typeof h.hy, 'number'); + assert.equal(typeof h.hm, 'number'); + assert.equal(typeof h.hd, 'number'); +}); + +test('isValidHijri returns true for in-range date', () => { + const valid = dayjs(D_RAMADAN_1444).isValidHijri(); + assert.equal(valid, true); +}); + +test('fromHijri throws for out-of-range UAQ date', () => { + // 1301 is before the UAQ table begins (coverage starts at 1318) + assert.throws(() => dayjs.fromHijri(1301, 1, 1), /Invalid or out-of-range/); +}); + +test('formatHijri passthrough: iYYYY YYYY contains both Hijri and Gregorian year', () => { + const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY YYYY'); + // Should contain '1444' (Hijri) and '2023' (Gregorian) + assert.ok(result.includes('1444'), `Expected Hijri year 1444 in: ${result}`); + assert.ok(result.includes('2023'), `Expected Gregorian year 2023 in: ${result}`); +}); + +console.log(`\n${passed}/${total} tests passed`); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e05618c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..9b9cafa --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + outDir: 'dist', + splitting: false, + sourcemap: true, + target: 'es2020', + platform: 'node', + external: ['dayjs', 'hijri-core'], + outExtension({ format }) { + return { js: format === 'esm' ? '.mjs' : '.cjs' }; + }, +});