From 295dbf86805cb4cd3dca7282e8e5c6729198891d Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 25 Feb 2026 14:15:18 -0500 Subject: [PATCH] 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. --- .editorconfig | 15 + .github/workflows/ci.yml | 62 +++ .github/workflows/wiki-sync.yml | 22 + .gitignore | 12 + .npmrc | 1 + .nvmrc | 1 + .wiki/API-Reference.md | 182 ++++++ .wiki/Architecture.md | 89 +++ .wiki/Home.md | 47 ++ CHANGELOG.md | 20 + LICENSE | 21 + README.md | 138 +++++ package.json | 60 ++ pnpm-lock.yaml | 942 ++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + src/index.ts | 145 +++++ src/types.ts | 1 + test-cjs.cjs | 75 +++ test.mjs | 129 +++++ tsconfig.json | 17 + tsup.config.ts | 17 + 21 files changed, 1998 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/wiki-sync.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .wiki/API-Reference.md create mode 100644 .wiki/Architecture.md create mode 100644 .wiki/Home.md create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 src/index.ts create mode 100644 src/types.ts create mode 100644 test-cjs.cjs create mode 100644 test.mjs create mode 100644 tsconfig.json create mode 100644 tsup.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f849911 --- /dev/null +++ b/.editorconfig @@ -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 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..b69d9a4 --- /dev/null +++ b/.github/workflows/wiki-sync.yml @@ -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 }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b01741f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +*.tgz +*.log +.DS_Store +.claude/ +.env +.env.* + +# AI agent directories +.cursor/ +.copilot/ 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..5590cb0 --- /dev/null +++ b/.wiki/API-Reference.md @@ -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) diff --git a/.wiki/Architecture.md b/.wiki/Architecture.md new file mode 100644 index 0000000..11fc4e6 --- /dev/null +++ b/.wiki/Architecture.md @@ -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) diff --git a/.wiki/Home.md b/.wiki/Home.md new file mode 100644 index 0000000..08d1011 --- /dev/null +++ b/.wiki/Home.md @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f07867d --- /dev/null +++ b/CHANGELOG.md @@ -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 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..fb243b4 --- /dev/null +++ b/README.md @@ -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. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2400631 --- /dev/null +++ b/package.json @@ -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" } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ad320b8 --- /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 + 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: {} 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..c91174d --- /dev/null +++ b/src/index.ts @@ -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).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'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5337e73 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type { HijriDate, ConversionOptions } from 'hijri-core'; diff --git a/test-cjs.cjs b/test-cjs.cjs new file mode 100644 index 0000000..ea7ae06 --- /dev/null +++ b/test-cjs.cjs @@ -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`); diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..8d40f51 --- /dev/null +++ b/test.mjs @@ -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`); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71c744d --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..956cd6b --- /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: ['moment', 'hijri-core'], + outExtension({ format }) { + return { js: format === 'esm' ? '.mjs' : '.cjs' }; + }, +});