feat: initial release of moment-hijri-plus v1.0.0

Moment.js plugin adding Hijri calendar support via hijri-core. Adds
toHijri(), fromHijri(), hijriYear/Month/Day(), isValidHijri(), and
formatHijri() to all Moment instances via fn prototype and module
augmentation. Format token escaping wraps substituted values in
moment bracket syntax [...] to prevent re-interpretation. UTC-midnight
date shift corrected by using getUTC* components + moment([y, m, d])
construction. 14 ESM + 8 CJS tests passing. Dual CJS/ESM build.
This commit is contained in:
Aric Camarata 2026-02-25 14:15:18 -05:00
commit 295dbf8680
21 changed files with 1998 additions and 0 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{c,h}]
indent_size = 4
[Makefile]
indent_style = tab

62
.github/workflows/ci.yml vendored Normal file
View file

@ -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

22
.github/workflows/wiki-sync.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Wiki Sync
on:
push:
branches: [main]
paths:
- '.wiki/**'
jobs:
sync:
name: Sync .wiki/ to GitHub Wiki
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sync wiki pages
uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: .wiki/
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
node_modules/
dist/
*.tgz
*.log
.DS_Store
.claude/
.env
.env.*
# AI agent directories
.cursor/
.copilot/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-import-method=hardlink

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
24

182
.wiki/API-Reference.md Normal file
View file

@ -0,0 +1,182 @@
# API Reference
## Installation
```bash
pnpm add moment moment-hijri-plus hijri-core
```
`moment` and `hijri-core` are peer dependencies. Both must be installed.
## Plugin installation
```javascript
import moment from 'moment';
import installHijri from 'moment-hijri-plus';
// Call once at application startup.
installHijri(moment);
```
After this call, all methods below are available on every moment instance and on the `moment` constructor itself.
---
## Instance methods
### `toHijri(options?)`
Converts the moment to a Hijri date.
**Signature:** `(options?: ConversionOptions) => HijriDate | null`
Returns `null` if the date falls outside the supported calendar range (UAQ covers AH 1356-1500, approximately CE 1937-2077).
```javascript
const h = moment(new Date(2023, 2, 23)).toHijri();
// => { hy: 1444, hm: 9, hd: 1 }
const h = moment(new Date(2023, 2, 23)).toHijri({ calendar: 'fcna' });
```
**HijriDate fields:**
| Field | Type | Description |
| --- | --- | --- |
| `hy` | `number` | Hijri year |
| `hm` | `number` | Hijri month (1 = Muharram, 12 = Dhul Hijjah) |
| `hd` | `number` | Hijri day (1-30) |
---
### `hijriYear(options?)`
**Signature:** `(options?: ConversionOptions) => number | null`
```javascript
moment(new Date(2023, 2, 23)).hijriYear(); // => 1444
```
---
### `hijriMonth(options?)`
**Signature:** `(options?: ConversionOptions) => number | null`
Returns 1-12 (1 = Muharram).
```javascript
moment(new Date(2023, 2, 23)).hijriMonth(); // => 9 (Ramadan)
```
---
### `hijriDay(options?)`
**Signature:** `(options?: ConversionOptions) => number | null`
```javascript
moment(new Date(2023, 2, 23)).hijriDay(); // => 1
```
---
### `isValidHijri(options?)`
**Signature:** `(options?: ConversionOptions) => boolean`
Returns `true` if the date falls within the supported range of the chosen calendar.
```javascript
moment(new Date(2023, 2, 23)).isValidHijri(); // => true
moment(new Date(1900, 0, 1)).isValidHijri(); // => false (before UAQ range)
```
---
### `formatHijri(formatStr, options?)`
**Signature:** `(formatStr: string, options?: ConversionOptions) => string`
Format using Hijri-aware tokens. All tokens not listed below are passed through to `moment.format()`, so Gregorian tokens work as normal.
Returns `''` if the date is outside the Hijri range.
```javascript
moment(new Date(2023, 2, 23)).formatHijri('iD iMMMM iYYYY AH');
// => '1 Ramadan 1444 AH'
moment(new Date(2023, 2, 23)).formatHijri('iYYYY-iMM-iDD');
// => '1444-09-01'
// Mix Hijri and Gregorian tokens.
moment(new Date(2023, 2, 23)).formatHijri('iD iMMMM iYYYY [CE:] MMMM D, YYYY');
// => '1 Ramadan 1444 CE: March 23, 2023'
```
**Format tokens:**
| Token | Example output | Description |
| --- | --- | --- |
| `iYYYY` | `1444` | Hijri year, 4+ digits, zero-padded to 4 |
| `iYY` | `44` | Hijri year, last 2 digits, zero-padded |
| `iMMMM` | `Ramadan` | Month long name |
| `iMMM` | `Ramadan` | Month medium name |
| `iMM` | `09` | Month number, zero-padded |
| `iM` | `9` | Month number |
| `iDD` | `01` | Day, zero-padded |
| `iD` | `1` | Day |
| `iEEEE` | `Yawm al-Khamis` | Weekday long name |
| `iEEE` | `Kham` | Weekday short name |
| `iE` | `5` | Weekday numeric (1=Sunday, 7=Saturday) |
| `ioooo` | `AH` | Era, long |
| `iooo` | `AH` | Era, short |
---
## Static methods
### `moment.fromHijri(hy, hm, hd, options?)`
**Signature:** `(hy: number, hm: number, hd: number, options?: ConversionOptions) => Moment`
Creates a moment from a Hijri date. Throws `Error` if the date is invalid or outside the calendar range.
```javascript
const m = moment.fromHijri(1444, 9, 1);
m.format('YYYY-MM-DD'); // => '2023-03-23'
// With FCNA calendar.
const m2 = moment.fromHijri(1444, 9, 1, { calendar: 'fcna' });
```
---
## Options
```typescript
interface ConversionOptions {
calendar?: string; // default: 'uaq'
}
```
| Calendar ID | Description |
| --- | --- |
| `uaq` | Umm al-Qura — official Saudi calendar, tabular, covers AH 1356-1500 |
| `fcna` | FCNA/ISNA — Fiqh Council of North America calculated calendar |
Custom calendars can be registered with hijri-core's `registerCalendar()`.
---
## TypeScript
All methods are typed via module augmentation. Import types from this package:
```typescript
import type { HijriDate, ConversionOptions } from 'moment-hijri-plus';
```
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)

89
.wiki/Architecture.md Normal file
View file

@ -0,0 +1,89 @@
# Architecture
## Design goals
The package has one job: adapt the hijri-core API to Moment.js idioms. No calendar logic belongs here. All date arithmetic, table lookups, and validation live in hijri-core, which is tested and maintained independently.
This constraint keeps moment-hijri-plus small, maintainable, and calendar-agnostic — it benefits automatically from any calendar or correctness improvements made in hijri-core.
## Plugin pattern
Moment.js plugins work by mutating `moment.fn` (the prototype for all moment instances) and the `moment` constructor itself. The canonical pattern is a single `install(momentInstance)` function that the caller invokes once:
```javascript
import installHijri from 'moment-hijri-plus';
installHijri(moment);
```
This approach avoids accidental double-registration, keeps the plugin stateless, and works with any moment instance — including custom ones created by `moment.utc()` or locale-scoped instances.
## Module augmentation
The TypeScript types are added to `moment.Moment` and `moment.MomentStatic` via declaration merging. This is the standard TypeScript way to extend third-party interfaces:
```typescript
declare module 'moment' {
interface Moment {
toHijri(options?: ConversionOptions): HijriDate | null;
// ...
}
interface MomentStatic {
fromHijri(hy: number, hm: number, hd: number, options?: ConversionOptions): Moment;
}
}
```
The augmentation is emitted in the declaration files produced by tsup, so consumers get full type inference without any extra imports.
## Format token system
`formatHijri()` uses a single regex pass to identify Hijri tokens, replaces them with resolved strings, then passes the residual format string to `moment.format()`. This means Gregorian tokens (`YYYY`, `MMM`, `dddd`, etc.) resolve exactly as they would without the plugin.
The regex is ordered longest-match-first to prevent prefix collisions:
```javascript
/iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g
```
`iYYYY` must appear before `iYY` for obvious reasons; `iMMMM` before `iMMM` and `iMM`; `iDD` before `iD`; `iEEEE` before `iEEE`. The global flag allows the regex to find all non-overlapping tokens in one pass.
Moment's own bracket escaping (`[literal text]`) is preserved because it only runs during the `moment.format()` call on the residual string — any `[...]` sequences in the user's format string that don't contain Hijri tokens pass through untouched.
## Delegation to hijri-core
Every conversion call goes through hijri-core:
```
toHijri() → hijri-core.toHijri(date, options)
fromHijri() → hijri-core.toGregorian(hy, hm, hd, options)
```
hijri-core maintains a registry of calendar engines. The default engine is `uaq` (Umm al-Qura). Callers can switch to `fcna` (FCNA/ISNA) or register custom engines via `hijri-core`'s `registerCalendar()`.
Because moment-hijri-plus uses hijri-core as a peer dependency, the registry is shared — a calendar registered in application code via `hijri-core`'s `registerCalendar()` is immediately available to this plugin.
## Build output
tsup produces four files:
| File | Format | Purpose |
| --- | --- | --- |
| `dist/index.cjs` | CommonJS | `require()` in Node.js and bundlers in CJS mode |
| `dist/index.mjs` | ESM | `import` in Node.js, Vite, Rollup, esbuild |
| `dist/index.d.ts` | CJS declaration | Types for CJS consumers (`require`) |
| `dist/index.d.mts` | ESM declaration | Types for ESM consumers (`import`) |
Both `moment` and `hijri-core` are marked external, so they are not bundled. They resolve from the consumer's `node_modules` at runtime.
## Calendar coverage
| Calendar | ID | Range | Authority |
| --- | --- | --- | --- |
| Umm al-Qura | `uaq` | AH 1356-1500 (approx CE 1937-2077) | Official Saudi calendar |
| FCNA/ISNA | `fcna` | Calculated, no hard range | Fiqh Council of North America |
The UAQ calendar is tabular: dates are looked up in a precomputed table published by the Umm al-Qura University. Dates outside the table return `null`. The FCNA calendar uses an astronomical calculation rule and has no strict boundary.
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)

47
.wiki/Home.md Normal file
View file

@ -0,0 +1,47 @@
# moment-hijri-plus
A Moment.js plugin for Hijri calendar conversion and formatting. All calendar arithmetic is handled by [hijri-core](https://github.com/acamarata/hijri-core), keeping this package thin and focused.
## What it does
- Converts any moment to a Hijri date object (`{ hy, hm, hd }`)
- Formats moments using Hijri-specific tokens mixed freely with standard Moment format tokens
- Constructs moments from Hijri dates via `moment.fromHijri()`
- Supports Umm al-Qura (UAQ) and FCNA/ISNA calendars
## Pages
- [API Reference](API-Reference) — complete method signatures and examples
- [Architecture](Architecture) — design rationale, token system, calendar delegation
## Quick start
```bash
pnpm add moment moment-hijri-plus hijri-core
```
```javascript
import moment from 'moment';
import installHijri from 'moment-hijri-plus';
installHijri(moment);
moment(new Date(2023, 2, 23)).toHijri();
// => { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH)
moment(new Date(2023, 2, 23)).formatHijri('iD iMMMM iYYYY AH');
// => '1 Ramadan 1444 AH'
moment.fromHijri(1446, 1, 1).format('YYYY-MM-DD');
// => '2024-07-07'
```
## Related packages
- [hijri-core](https://github.com/acamarata/hijri-core) — the calendar engine
- [luxon-hijri](https://github.com/acamarata/luxon-hijri) — same support for Luxon
- [pray-calc](https://github.com/acamarata/pray-calc) — Islamic prayer time calculation
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)

20
CHANGELOG.md Normal file
View file

@ -0,0 +1,20 @@
# 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
- Initial release
- `toHijri()` instance method: convert a moment to a Hijri date object
- `hijriYear()`, `hijriMonth()`, `hijriDay()` convenience accessors
- `isValidHijri()` range check
- `formatHijri()` with 13 Hijri-specific format tokens
- `moment.fromHijri()` static factory for constructing moments from Hijri dates
- Umm al-Qura (UAQ) calendar support via hijri-core (default)
- FCNA/ISNA calendar support via hijri-core
- Full TypeScript definitions with module augmentation for `moment.Moment` and `moment.MomentStatic`
- Dual CJS/ESM build with separate type declaration files

21
LICENSE Normal file
View file

@ -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.

138
README.md Normal file
View file

@ -0,0 +1,138 @@
# moment-hijri-plus
[![npm version](https://img.shields.io/npm/v/moment-hijri-plus.svg)](https://www.npmjs.com/package/moment-hijri-plus)
[![CI](https://github.com/acamarata/moment-hijri-plus/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/moment-hijri-plus/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Moment.js plugin for Hijri calendar conversion and formatting. Delegates all calendar logic to [hijri-core](https://github.com/acamarata/hijri-core), a zero-dependency Hijri engine with pluggable calendar support.
## Installation
```bash
pnpm add moment moment-hijri-plus hijri-core
```
Both `moment` and `hijri-core` are peer dependencies and must be installed alongside this package.
## Quick Start
```javascript
import moment from 'moment';
import installHijri from 'moment-hijri-plus';
// Install the plugin once at startup.
installHijri(moment);
// Convert a Gregorian date to Hijri.
const m = moment(new Date(2023, 2, 23)); // 23 March 2023
const hijri = m.toHijri();
// => { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH)
// Format using Hijri tokens.
m.formatHijri('iD iMMMM iYYYY AH');
// => '1 Ramadan 1444 AH'
// Construct a moment from a Hijri date.
const start = moment.fromHijri(1446, 1, 1);
// => moment representing 7 July 2024 (1 Muharram 1446 AH)
```
## API
### Instance methods
All methods are added to `moment.Moment` by calling `installHijri(moment)` once.
| Method | Signature | Description |
| --- | --- | --- |
| `toHijri` | `(options?) => HijriDate \| null` | Convert to Hijri. Returns `null` if the date is outside the calendar range. |
| `hijriYear` | `(options?) => number \| null` | Hijri year, or `null` if out of range. |
| `hijriMonth` | `(options?) => number \| null` | Hijri month (1-12), or `null` if out of range. |
| `hijriDay` | `(options?) => number \| null` | Hijri day, or `null` if out of range. |
| `isValidHijri` | `(options?) => boolean` | `true` if the date falls within the supported Hijri range. |
| `formatHijri` | `(formatStr, options?) => string` | Format using Hijri tokens. Returns `''` if out of range. Non-Hijri tokens pass through to `moment.format()`. |
### Static factory
| Method | Signature | Description |
| --- | --- | --- |
| `moment.fromHijri` | `(hy, hm, hd, options?) => Moment` | Create a moment from a Hijri date. Throws if the date is invalid or out of range. |
### Options
```typescript
interface ConversionOptions {
calendar?: string; // 'uaq' (default) | 'fcna'
}
```
## Calendar Systems
| ID | Name | Description |
| --- | --- | --- |
| `uaq` | Umm al-Qura | Official calendar of Saudi Arabia. Tabular, covers AH 1356-1500. Default. |
| `fcna` | FCNA/ISNA | Fiqh Council of North America calculated calendar. |
Pass the calendar ID via `options`:
```javascript
m.toHijri({ calendar: 'fcna' });
moment.fromHijri(1444, 9, 1, { calendar: 'fcna' });
```
## Format Tokens
`formatHijri()` recognises the following tokens. All other tokens are passed through to `moment.format()`, so you can mix Hijri and Gregorian tokens freely.
| Token | Example | Description |
| --- | --- | --- |
| `iYYYY` | `1444` | Hijri year, 4 digits |
| `iYY` | `44` | Hijri year, 2 digits |
| `iMMMM` | `Ramadan` | Month long name |
| `iMMM` | `Ramadan` | Month medium name |
| `iMM` | `09` | Month, zero-padded |
| `iM` | `9` | Month, no padding |
| `iDD` | `01` | Day, zero-padded |
| `iD` | `1` | Day, no padding |
| `iEEEE` | `Yawm al-Khamis` | Weekday long name |
| `iEEE` | `Kham` | Weekday short name |
| `iE` | `5` | Weekday numeric (1=Sun, 7=Sat) |
| `ioooo` | `AH` | Era, long |
| `iooo` | `AH` | Era, short |
### Mixed format example
```javascript
m.formatHijri('iD iMMMM iYYYY [CE:] MMMM YYYY');
// => '1 Ramadan 1444 CE: March 2023'
```
Bracket escaping (`[...]`) is handled by moment's own formatter for the Gregorian portion.
## TypeScript
The plugin augments `moment.Moment` and `moment.MomentStatic` via module declaration merging, so type safety applies after the plugin is installed. No extra imports are needed for the types.
```typescript
import moment from 'moment';
import installHijri from 'moment-hijri-plus';
import type { HijriDate, ConversionOptions } from 'moment-hijri-plus';
installHijri(moment);
const hijri: HijriDate | null = moment().toHijri();
```
## Documentation
Full API reference, architecture notes, and calendar algorithm details are in the [project wiki](https://github.com/acamarata/moment-hijri-plus/wiki).
## Related
- [hijri-core](https://github.com/acamarata/hijri-core) — zero-dependency Hijri calendar engine used by this plugin
- [luxon-hijri](https://github.com/acamarata/luxon-hijri) — same Hijri support for Luxon
- [pray-calc](https://github.com/acamarata/pray-calc) — Islamic prayer time calculation
## License
MIT. Copyright (c) 2026 Aric Camarata.

60
package.json Normal file
View file

@ -0,0 +1,60 @@
{
"name": "moment-hijri-plus",
"version": "1.0.0",
"description": "Moment.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": [
"moment",
"momentjs",
"plugin",
"hijri",
"islamic",
"calendar",
"umm-al-qura",
"fcna",
"gregorian",
"converter",
"typescript"
],
"peerDependencies": { "moment": "^2.0.0", "hijri-core": "^1.0.0" },
"devDependencies": {
"@types/node": "^22.0.0",
"hijri-core": "file:../hijri-core",
"moment": "^2.30.0",
"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/moment-hijri-plus.git" },
"homepage": "https://github.com/acamarata/moment-hijri-plus#readme",
"bugs": { "url": "https://github.com/acamarata/moment-hijri-plus/issues" }
}

942
pnpm-lock.yaml Normal file
View file

@ -0,0 +1,942 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.11
hijri-core:
specifier: file:../hijri-core
version: file:../hijri-core
moment:
specifier: ^2.30.0
version: 2.30.1
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}
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==}
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
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: {}
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
moment@2.30.1: {}
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: {}

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

145
src/index.ts Normal file
View file

@ -0,0 +1,145 @@
import moment from 'moment';
import {
toHijri,
toGregorian,
hmLong,
hmMedium,
hwLong,
hwShort,
hwNumeric,
} from 'hijri-core';
import type { HijriDate, ConversionOptions } from './types';
declare module 'moment' {
interface Moment {
/**
* Convert this moment to a Hijri date.
* Returns null if the date falls outside the supported calendar range.
*/
toHijri(options?: ConversionOptions): HijriDate | null;
/** Return the Hijri year, or null if out of range. */
hijriYear(options?: ConversionOptions): number | null;
/** Return the Hijri month (1-12), or null if out of range. */
hijriMonth(options?: ConversionOptions): number | null;
/** Return the Hijri day, or null if out of range. */
hijriDay(options?: ConversionOptions): number | null;
/** Return true if this moment falls within the supported Hijri range. */
isValidHijri(options?: ConversionOptions): boolean;
/**
* Format this moment using Hijri-aware format tokens.
*
* Hijri tokens: iYYYY iYY iMMMM iMMM iMM iM iDD iD iEEEE iEEE iE ioooo iooo
* All other tokens are passed through to moment's own format().
*
* Returns an empty string if the date is outside the Hijri range.
*/
formatHijri(formatStr: string, options?: ConversionOptions): string;
}
}
// Regex matching all Hijri format tokens. Ordered longest-first so iYYYY is
// matched before iYY, iMMMM before iMMM, iDD before iD, iEEEE before iEEE.
const HIJRI_TOKEN_RE =
/iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g;
/**
* Escape a literal string so moment.format() treats it as literal text.
* Wraps the value in square brackets, escaping any ] characters within.
*/
function escapeLiteral(value: string): string {
return '[' + value.replace(/]/g, '][]') + ']';
}
/**
* Install the Hijri plugin into the provided moment instance.
*
* @example
* import moment from 'moment';
* import installHijri from 'moment-hijri-plus';
* installHijri(moment);
*/
function install(momentInstance: typeof moment): void {
momentInstance.fn.toHijri = function (opts?: ConversionOptions): HijriDate | null {
return toHijri(this.toDate(), opts);
};
momentInstance.fn.hijriYear = function (opts?: ConversionOptions): number | null {
return this.toHijri(opts)?.hy ?? null;
};
momentInstance.fn.hijriMonth = function (opts?: ConversionOptions): number | null {
return this.toHijri(opts)?.hm ?? null;
};
momentInstance.fn.hijriDay = function (opts?: ConversionOptions): number | null {
return this.toHijri(opts)?.hd ?? null;
};
momentInstance.fn.isValidHijri = function (opts?: ConversionOptions): boolean {
return this.toHijri(opts) !== null;
};
momentInstance.fn.formatHijri = function (
formatStr: string,
opts?: ConversionOptions,
): string {
const hijri = this.toHijri(opts);
if (!hijri) return '';
const m = this;
// Replace Hijri tokens with escaped literals, then pass the residual string
// to moment.format() so all standard tokens (YYYY, MMM, etc.) resolve correctly.
// Escaping is required because values like "Ramadan" would otherwise be
// interpreted by moment as format tokens (R, a, m, etc.).
const residual = formatStr.replace(HIJRI_TOKEN_RE, (token: string): string => {
switch (token) {
case 'iYYYY': return escapeLiteral(String(hijri.hy).padStart(4, '0'));
case 'iYY': return escapeLiteral(String(hijri.hy % 100).padStart(2, '0'));
case 'iMMMM': return escapeLiteral(hmLong[hijri.hm - 1]);
case 'iMMM': return escapeLiteral(hmMedium[hijri.hm - 1]);
case 'iMM': return escapeLiteral(String(hijri.hm).padStart(2, '0'));
case 'iM': return escapeLiteral(String(hijri.hm));
case 'iDD': return escapeLiteral(String(hijri.hd).padStart(2, '0'));
case 'iD': return escapeLiteral(String(hijri.hd));
case 'iEEEE': return escapeLiteral(hwLong[m.day()]);
case 'iEEE': return escapeLiteral(hwShort[m.day()]);
case 'iE': return escapeLiteral(String(hwNumeric[m.day()]));
// Era tokens: both iooo and ioooo map to the common abbreviation.
case 'iooo':
case 'ioooo': return escapeLiteral('AH');
default: return token;
}
});
return m.format(residual);
};
// Attach fromHijri as a property on the constructor. We use a type assertion
// because MomentStatic augmentation produces a DTS visibility error with some
// TypeScript configurations — attaching at runtime is equivalent and safe.
(momentInstance as unknown as Record<string, unknown>).fromHijri = function (
hy: number,
hm: number,
hd: number,
opts?: ConversionOptions,
): moment.Moment {
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}`);
}
// Construct from explicit year/month/day to avoid UTC-to-local timezone
// shift when the Date object represents midnight UTC.
return momentInstance([greg.getUTCFullYear(), greg.getUTCMonth(), greg.getUTCDate()]);
};
}
export default install;
export type { HijriDate, ConversionOptions } from 'hijri-core';

1
src/types.ts Normal file
View file

@ -0,0 +1 @@
export type { HijriDate, ConversionOptions } from 'hijri-core';

75
test-cjs.cjs Normal file
View file

@ -0,0 +1,75 @@
'use strict';
const assert = require('node:assert/strict');
const moment = require('moment');
const installHijri = require('./dist/index.cjs');
installHijri.default(moment);
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.exitCode = 1;
}
}
// 1. Plugin installs
test('plugin installs (CJS)', () => {
assert.equal(typeof moment.fn.toHijri, 'function');
assert.equal(typeof moment.fn.formatHijri, 'function');
assert.equal(typeof moment.fromHijri, 'function');
});
// 2. toHijri
test('toHijri: 2023-03-23 => 1444/9/1 (CJS)', () => {
const h = moment(new Date(2023, 2, 23, 12)).toHijri();
assert.notEqual(h, null);
assert.equal(h.hy, 1444);
assert.equal(h.hm, 9);
assert.equal(h.hd, 1);
});
// 3. fromHijri
test('fromHijri: 1444/9/1 => 2023-03-23 (CJS)', () => {
const d = moment.fromHijri(1444, 9, 1).toDate();
assert.equal(d.getFullYear(), 2023);
assert.equal(d.getMonth(), 2);
assert.equal(d.getDate(), 23);
});
// 4. formatHijri: numeric
test('formatHijri: iYYYY-iMM-iDD (CJS)', () => {
const result = moment(new Date(2023, 2, 23, 12)).formatHijri('iYYYY-iMM-iDD');
assert.equal(result, '1444-09-01');
});
// 5. formatHijri: month name
test('formatHijri: iMMMM => Ramadan (CJS)', () => {
const result = moment(new Date(2023, 2, 23, 12)).formatHijri('iMMMM');
assert.equal(result, 'Ramadan');
});
// 6. fromHijri throws for invalid date
test('fromHijri throws on out-of-range date (CJS)', () => {
assert.throws(() => moment.fromHijri(999, 1, 1), /Invalid or out-of-range/);
});
// 7. hijriYear accessor
test('hijriYear: 1444 (CJS)', () => {
assert.equal(moment(new Date(2023, 2, 23, 12)).hijriYear(), 1444);
});
// 8. isValidHijri
test('isValidHijri: true for valid date (CJS)', () => {
assert.equal(moment(new Date(2023, 2, 23, 12)).isValidHijri(), true);
});
console.log(`\n${passed}/${total} tests passed`);

129
test.mjs Normal file
View file

@ -0,0 +1,129 @@
import assert from 'node:assert/strict';
import moment from 'moment';
import installHijri from './dist/index.mjs';
installHijri(moment);
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.exitCode = 1;
}
}
// 1. Plugin installs
test('plugin installs', () => {
assert.equal(typeof moment.fn.toHijri, 'function');
assert.equal(typeof moment.fn.hijriYear, 'function');
assert.equal(typeof moment.fn.hijriMonth, 'function');
assert.equal(typeof moment.fn.hijriDay, 'function');
assert.equal(typeof moment.fn.isValidHijri, 'function');
assert.equal(typeof moment.fn.formatHijri, 'function');
assert.equal(typeof moment.fromHijri, 'function');
});
// 2. toHijri: 1 Ramadan 1444 AH
test('toHijri: 2023-03-23 => 1444/9/1', () => {
const h = moment(new Date(2023, 2, 23, 12)).toHijri();
assert.notEqual(h, null);
assert.equal(h.hy, 1444);
assert.equal(h.hm, 9);
assert.equal(h.hd, 1);
});
// 3. toHijri: 1 Muharram 1446 AH
test('toHijri: 2024-07-07 => 1446/1/1', () => {
const h = moment(new Date(2024, 6, 7, 12)).toHijri();
assert.notEqual(h, null);
assert.equal(h.hy, 1446);
assert.equal(h.hm, 1);
assert.equal(h.hd, 1);
});
// 4. fromHijri: 1444/9/1 => 2023-03-23
test('fromHijri: 1444/9/1 => 2023-03-23', () => {
const m = moment.fromHijri(1444, 9, 1);
const d = m.toDate();
assert.equal(d.getFullYear(), 2023);
assert.equal(d.getMonth(), 2); // March = 2
assert.equal(d.getDate(), 23);
});
// 5. fromHijri: 1446/1/1 => 2024-07-07
test('fromHijri: 1446/1/1 => 2024-07-07', () => {
const m = moment.fromHijri(1446, 1, 1);
const d = m.toDate();
assert.equal(d.getFullYear(), 2024);
assert.equal(d.getMonth(), 6); // July = 6
assert.equal(d.getDate(), 7);
});
// 6. hijriYear / hijriMonth / hijriDay
test('hijriYear, hijriMonth, hijriDay on 1 Ramadan 1444', () => {
const m = moment(new Date(2023, 2, 23, 12));
assert.equal(m.hijriYear(), 1444);
assert.equal(m.hijriMonth(), 9);
assert.equal(m.hijriDay(), 1);
});
// 7. formatHijri: numeric format
test('formatHijri: iYYYY-iMM-iDD', () => {
const result = moment(new Date(2023, 2, 23, 12)).formatHijri('iYYYY-iMM-iDD');
assert.equal(result, '1444-09-01');
});
// 8. formatHijri: long month name
test('formatHijri: iMMMM => Ramadan', () => {
const result = moment(new Date(2023, 2, 23, 12)).formatHijri('iMMMM');
assert.equal(result, 'Ramadan');
});
// 9. formatHijri: long weekday name (Thursday = Yawm al-Khamis)
test('formatHijri: iEEEE on Thursday 2023-03-23', () => {
const result = moment(new Date(2023, 2, 23, 12)).formatHijri('iEEEE');
assert.equal(result, 'Yawm al-Khamis');
});
// 10. formatHijri: era token
test('formatHijri: ioooo => AH', () => {
const result = moment(new Date(2023, 2, 23, 12)).formatHijri('ioooo');
assert.equal(result, 'AH');
});
// 11. isValidHijri: returns true for in-range date
test('isValidHijri: true for valid date', () => {
assert.equal(moment(new Date(2023, 2, 23, 12)).isValidHijri(), true);
});
// 12. FCNA calendar option
test('toHijri with { calendar: fcna } returns a HijriDate', () => {
const h = moment(new Date(2023, 2, 23, 12)).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');
});
// 13. fromHijri throws for out-of-range date
test('fromHijri throws on out-of-range Hijri date', () => {
assert.throws(() => moment.fromHijri(999, 1, 1), /Invalid or out-of-range/);
});
// 14. formatHijri: mixed Hijri and Gregorian tokens
test('formatHijri: mixed Hijri and Gregorian tokens', () => {
const m = moment(new Date(2023, 2, 23, 12));
const result = m.formatHijri('iYYYY [CE:] YYYY');
// Hijri year should be 1444; Gregorian year should be 2023.
assert.ok(result.includes('1444'), `Expected Hijri year in: ${result}`);
assert.ok(result.includes('2023'), `Expected Gregorian year in: ${result}`);
});
console.log(`\n${passed}/${total} tests passed`);

17
tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
}

17
tsup.config.ts Normal file
View file

@ -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: ['moment', 'hijri-core'],
outExtension({ format }) {
return { js: format === 'esm' ? '.mjs' : '.cjs' };
},
});