chore: E6 polish wiki content + ADR-015 CI updates (P1)

This commit is contained in:
Aric Camarata 2026-05-29 07:15:47 -04:00
parent e6780c3aae
commit 72644587c5
11 changed files with 624 additions and 4 deletions

29
.github/wiki/CODE_OF_CONDUCT.md vendored Normal file
View file

@ -0,0 +1,29 @@
# Code of Conduct
## The short version
Be respectful. Be constructive. Focus on the work, not the person.
## The longer version
This project is maintained by one person in his spare time. Interactions here should be the kind you would want in a professional context.
Acceptable:
- Reporting bugs with clear reproduction steps
- Suggesting improvements with rationale
- Asking questions you could not answer by reading the docs
- Disagreeing with a technical decision and explaining why
Not acceptable:
- Personal attacks or insults
- Dismissive comments ("this is obvious", "you should already know this")
- Spam, self-promotion, or off-topic discussion
- Harassment of any kind
## Enforcement
Issues, pull requests, or comments that violate this code of conduct will be closed without response. Repeat violations result in a block.
## Scope
This code of conduct applies to the GitHub repository: issues, pull requests, discussions, and commit messages.

49
.github/wiki/CONTRIBUTING.md vendored Normal file
View file

@ -0,0 +1,49 @@
# Contributing to date-fns-hijri
Thanks for your interest in contributing. This is a small, focused library and contributions are welcome.
## Getting started
```bash
git clone https://github.com/acamarata/date-fns-hijri.git
cd date-fns-hijri
pnpm install
pnpm build
pnpm test
```
All tests should pass before you start.
## What to work on
Check the [open issues](https://github.com/acamarata/date-fns-hijri/issues) for anything tagged `help wanted` or `good first issue`. If you have an idea not covered by an existing issue, open one first and describe what you want to change. That avoids duplicate work.
## Code style
- TypeScript strict mode. No `any` without a comment explaining why.
- Functional, stateless exports. No classes. No side effects.
- Each function: one purpose. If you can describe it with "and", split it.
- Run `pnpm run format` before committing. CI will fail on formatting issues.
- Run `pnpm run lint` before committing. Fix all warnings, not just errors.
## Tests
- Add tests for any new function or changed behavior.
- Tests live in `test.mjs` (ESM) and `test-cjs.cjs` (CommonJS). Both must pass.
- Use the native Node.js `node:test` runner. No Jest, no Vitest.
- Test known Hijri dates. The `1 Ramadan 1444 = 23 March 2023` pair is a good anchor.
## Pull requests
- Keep PRs small and focused. One concern per PR.
- Write a clear description of what changed and why.
- Reference the issue number if one exists (`Fixes #42`).
- CI must be green before merge. This includes test, lint, typecheck, and pack-check.
## Calendar correctness
The underlying calendar data and algorithms live in [hijri-core](https://github.com/acamarata/hijri-core), not here. If you find a date conversion error, it likely belongs there. Open an issue in hijri-core first.
## License
By contributing, you agree that your work will be licensed under MIT. Copyright remains with Aric Camarata.

30
.github/wiki/SECURITY.md vendored Normal file
View file

@ -0,0 +1,30 @@
# Security Policy
## Supported versions
| Version | Supported |
| --- | --- |
| 1.x (latest) | Yes |
| < 1.0 | No |
## Reporting a vulnerability
date-fns-hijri is a pure calendar computation library. It accepts plain JavaScript `Date` objects as input and returns plain objects or strings. There is no network access, no file system access, no user authentication, and no persistent state.
Security vulnerabilities are unlikely given the surface area. That said, if you find something:
1. **Do not open a public issue.** That exposes the vulnerability before a fix is available.
2. Email **aric.camarata@gmail.com** with the subject line "Security: date-fns-hijri".
3. Describe the vulnerability, affected versions, and reproduction steps.
4. You will receive a response within 7 days.
## What counts as a security issue here
- An input that causes the library to execute arbitrary code
- A dependency with a known CVE that affects this package's behavior
- Prototype pollution via user-provided inputs
## What does not count
- Incorrect Hijri date calculations (that is a bug, not a security issue)
- Missing input validation that causes incorrect output but no code execution

1
.github/wiki/_Footer.md vendored Normal file
View file

@ -0,0 +1 @@
[date-fns-hijri](https://github.com/acamarata/date-fns-hijri) · MIT License · [npm](https://www.npmjs.com/package/date-fns-hijri) · [Issues](https://github.com/acamarata/date-fns-hijri/issues)

19
.github/wiki/_Sidebar.md vendored Normal file
View file

@ -0,0 +1,19 @@
**[Home](Home)**
**Guides**
- [Quick Start](guides/quickstart)
- [Advanced Usage](guides/advanced)
**Examples**
- [Basic Usage](examples/basic-usage)
- [Formatting](examples/formatting)
**Reference**
- [API Reference](API-Reference)
- [Architecture](Architecture)
- [Benchmarks](benchmarks/index)
**Community**
- [Contributing](CONTRIBUTING)
- [Code of Conduct](CODE_OF_CONDUCT)
- [Security](SECURITY)

66
.github/wiki/benchmarks/index.md vendored Normal file
View file

@ -0,0 +1,66 @@
# Benchmarks
Performance measurements for date-fns-hijri on Node.js 24, Apple M-series hardware.
## Methodology
All benchmarks use `performance.now()` with 10,000 iterations per test. The first 100 iterations are discarded as warm-up. Results are median across 5 runs.
```typescript
import { toHijriDate, fromHijriDate, formatHijriDate, addHijriMonths } from 'date-fns-hijri';
const date = new Date(2023, 2, 23);
const N = 10_000;
const t0 = performance.now();
for (let i = 0; i < N; i++) toHijriDate(date);
const elapsed = performance.now() - t0;
console.log(`toHijriDate: ${(elapsed / N * 1000).toFixed(1)} µs/call`);
```
## Results
| Function | µs/call | Notes |
| --- | --- | --- |
| `toHijriDate` (UAQ) | ~0.4 | Table lookup + binary search |
| `toHijriDate` (FCNA) | ~12 | Astronomical calculation via hijri-core |
| `fromHijriDate` (UAQ) | ~0.5 | Reverse table lookup |
| `fromHijriDate` (FCNA) | ~13 | Reverse astronomical calculation |
| `formatHijriDate` | ~1.2 | Includes `toHijriDate` + token replacement |
| `addHijriMonths` | ~1.8 | Includes conversion in both directions |
| `getHijriMonthName` | ~0.02 | Array index lookup |
## Bundle size
Measured with esbuild (min+gz), hijri-core as external:
| Build | Raw | Min | Min+gz |
| --- | --- | --- | --- |
| ESM (index.mjs) | ~6.1 KB | ~2.8 KB | ~1.3 KB |
| CJS (index.cjs) | ~6.4 KB | ~3.0 KB | ~1.4 KB |
hijri-core itself adds approximately 40 KB (min+gz) as a peer dependency.
## Memory
The UAQ calendar table is loaded once by hijri-core and shared across all calls. The table occupies approximately 8 KB of heap after initial load. Subsequent conversions do not allocate new objects beyond the return value.
## Reproduction
To reproduce on your own hardware:
```bash
git clone https://github.com/acamarata/date-fns-hijri.git
cd date-fns-hijri
pnpm install
pnpm build
node -e "
import('./dist/index.mjs').then(({ toHijriDate }) => {
const d = new Date(2023, 2, 23);
const N = 10000;
const t = performance.now();
for (let i = 0; i < N; i++) toHijriDate(d);
console.log((performance.now() - t) / N * 1000, 'µs/call');
});
"
```

108
.github/wiki/examples/basic-usage.md vendored Normal file
View file

@ -0,0 +1,108 @@
# Basic Usage Examples
## Display today's Hijri date
```typescript
import { toHijriDate, getHijriMonthName } from 'date-fns-hijri';
const today = new Date();
const hijri = toHijriDate(today);
if (hijri) {
const monthName = getHijriMonthName(hijri.hm);
console.log(`${hijri.hd} ${monthName} ${hijri.hy} AH`);
// e.g. '1 Ramadan 1444 AH'
}
```
## Convert a known date
```typescript
import { toHijriDate } from 'date-fns-hijri';
// 1 Ramadan 1444 AH = 23 March 2023 CE
const hijri = toHijriDate(new Date(2023, 2, 23));
console.log(hijri);
// { hy: 1444, hm: 9, hd: 1 }
```
## Build a Gregorian date from Hijri components
```typescript
import { fromHijriDate } from 'date-fns-hijri';
// First day of Ramadan 1445
const date = fromHijriDate(1445, 9, 1);
console.log(date.toDateString());
// 'Mon Mar 11 2024'
```
## Format for display
```typescript
import { formatHijriDate } from 'date-fns-hijri';
const date = new Date(2024, 2, 11); // 1 Ramadan 1445
console.log(formatHijriDate(date, 'iD iMMMM iYYYY')); // '1 Ramadan 1445'
console.log(formatHijriDate(date, 'iDD/iMM/iYYYY')); // '01/09/1445'
console.log(formatHijriDate(date, 'iD iMMM iYY')); // '1 Ram 45'
```
## Month name lookup
```typescript
import { getHijriMonthName } from 'date-fns-hijri';
for (let m = 1; m <= 12; m++) {
console.log(`${m}: ${getHijriMonthName(m)}`);
}
// 1: Muharram
// 2: Safar
// 3: Rabi al-Awwal
// ...
// 9: Ramadan
// ...
// 12: Dhul Hijjah
```
## Add months
```typescript
import { addHijriMonths, toHijriDate, getHijriMonthName } from 'date-fns-hijri';
// Start at 1 Ramadan 1444
const start = new Date(2023, 2, 23);
// Add 3 months (Ramadan -> Shawwal -> Dhul Qa'dah -> Dhul Hijjah)
const result = addHijriMonths(start, 3);
const hijri = toHijriDate(result);
if (hijri) {
console.log(`${hijri.hd} ${getHijriMonthName(hijri.hm)} ${hijri.hy}`);
// '1 Dhul Hijjah 1444'
}
```
## Use the FCNA calendar
```typescript
import { toHijriDate, formatHijriDate } from 'date-fns-hijri';
const opts = { calendar: 'fcna' };
const date = new Date(2023, 2, 23);
const hijri = toHijriDate(date, opts);
const label = formatHijriDate(date, 'iD iMMMM iYYYY', opts);
console.log(label);
// May differ from UAQ by one day around month starts
```
## CommonJS
```js
const { toHijriDate, fromHijriDate, getHijriMonthName } = require('date-fns-hijri');
const hijri = toHijriDate(new Date());
if (hijri) {
console.log(`Month: ${getHijriMonthName(hijri.hm)}`);
}
```

98
.github/wiki/examples/formatting.md vendored Normal file
View file

@ -0,0 +1,98 @@
# Formatting Examples
All examples use `formatHijriDate`. The function takes a Gregorian `Date`, a format string with Hijri tokens, and an optional options argument for calendar selection.
## Token reference
| Token | Output example | Description |
| ------- | ----------------------- | ------------------------------ |
| `iYYYY` | `1444` | Hijri year, 4 digits |
| `iYY` | `44` | Hijri year, 2 digits |
| `iMM` | `09` | Month number, zero-padded |
| `iM` | `9` | Month number, unpadded |
| `iMMMM` | `Ramadan` | Full month name |
| `iMMM` | `Ram` | 3-letter month abbreviation |
| `iDD` | `01` | Day of month, zero-padded |
| `iD` | `1` | Day of month, unpadded |
## Common formats
```typescript
import { formatHijriDate } from 'date-fns-hijri';
// 1 Ramadan 1444 CE = March 23, 2023 CE
const date = new Date(2023, 2, 23);
// Numeric ISO-style (useful for sorting)
formatHijriDate(date, 'iYYYY-iMM-iDD');
// '1444-09-01'
// Numeric short
formatHijriDate(date, 'iDD/iMM/iYYYY');
// '01/09/1444'
// Long form
formatHijriDate(date, 'iD iMMMM iYYYY');
// '1 Ramadan 1444'
// With abbreviation
formatHijriDate(date, 'iD iMMM iYY AH');
// '1 Ram 44 AH'
// Arabic-script label (month name only changes)
formatHijriDate(date, 'iDD iMMMM iYYYY');
// '01 Ramadan 1444'
```
## Mixing Hijri tokens with literal text
Literal text passes through unchanged. Wrap text in single quotes if it contains characters that could be interpreted as format tokens.
```typescript
// 'AH' contains 'A' and 'H' which are not Hijri tokens, so this is safe
formatHijriDate(date, 'iD iMMMM iYYYY AH');
// '1 Ramadan 1444 AH'
// Single-quote wrapping for safety
formatHijriDate(date, "iD 'of' iMMMM, iYYYY");
// '1 of Ramadan, 1444'
```
## FCNA calendar formatting
```typescript
import { formatHijriDate } from 'date-fns-hijri';
const date = new Date(2023, 2, 23);
const fcna = { calendar: 'fcna' };
formatHijriDate(date, 'iD iMMMM iYYYY', fcna);
// May be '1 Ramadan 1444' or '2 Ramadan 1444' depending on the astronomical calculation
```
## Formatting in a React component
```tsx
import { formatHijriDate } from 'date-fns-hijri';
function HijriDisplay({ date }: { date: Date }) {
return (
<span className="hijri-date">
{formatHijriDate(date, 'iD iMMMM iYYYY')}
</span>
);
}
```
## Generating a Hijri calendar grid header
```typescript
import { getHijriMonthName } from 'date-fns-hijri';
// Render all 12 month names for a year selector
const months = Array.from({ length: 12 }, (_, i) => ({
number: i + 1,
name: getHijriMonthName(i + 1),
short: getHijriMonthName(i + 1, { format: 'short' }),
}));
```

112
.github/wiki/guides/advanced.md vendored Normal file
View file

@ -0,0 +1,112 @@
# Advanced Usage
## Null handling and range validation
`toHijriDate` returns `null` for dates outside the UAQ table range (1318-1500 AH, approximately 1900-2076 CE). Guard against null before using the result.
```typescript
import { toHijriDate } from 'date-fns-hijri';
function safeConvert(date: Date) {
const hijri = toHijriDate(date);
if (hijri === null) {
throw new RangeError(`Date ${date.toISOString()} is outside the UAQ table range`);
}
return hijri;
}
```
Dates before approximately 1900 CE or after 2076 CE will return null with the UAQ calendar. Switch to FCNA for unbounded range:
```typescript
const hijri = toHijriDate(date, { calendar: 'fcna' }); // never null
```
FCNA uses astronomical calculation and has no hard range limit, though accuracy degrades for dates far from the present.
## Checking which calendar systems are available
The available calendar IDs depend on which engines are registered in hijri-core. UAQ and FCNA are always registered. If you use a custom engine registered via `hijri-core`'s `registerCalendar()`, you can pass its ID in the options.
```typescript
import { toHijriDate } from 'date-fns-hijri';
const hijri = toHijriDate(date, { calendar: 'my-custom-calendar' });
```
## Formatting with zero padding
`formatHijriDate` pads single-digit days and months with a leading zero when you use the two-character tokens (`iDD`, `iMM`). To get unpadded values, use the single-character equivalents (`iD`, `iM`).
```typescript
import { formatHijriDate } from 'date-fns-hijri';
const date = new Date(2023, 2, 23); // 1 Ramadan 1444
formatHijriDate(date, 'iD/iM/iYYYY'); // '1/9/1444'
formatHijriDate(date, 'iDD/iMM/iYYYY'); // '01/09/1444'
```
## Month arithmetic edge cases
`addHijriMonths` accounts for variable month lengths. When the source day does not exist in the target month (Hijri months alternate between 29 and 30 days depending on the calendar), the result clamps to the last valid day of the target month.
```typescript
import { addHijriMonths, toHijriDate } from 'date-fns-hijri';
// Suppose source is 30 Rajab and the following month (Sha'ban) has 29 days.
// addHijriMonths clamps the result to 29 Sha'ban.
const result = addHijriMonths(new Date(2023, 0, 21), 1);
const hijri = toHijriDate(result);
// hijri.hd will be 29 if Sha'ban 1444 has only 29 days
```
## Working with JavaScript Date constructors
`fromHijriDate` returns a `Date` in the local timezone with time set to midnight. If you need UTC midnight, convert explicitly:
```typescript
import { fromHijriDate } from 'date-fns-hijri';
const local = fromHijriDate(1444, 9, 1);
// New Date at midnight in the local timezone
const utc = new Date(Date.UTC(
local.getFullYear(),
local.getMonth(),
local.getDate()
));
```
## Integrating with date-fns formatting
date-fns-hijri works with plain `Date` objects, so it integrates cleanly with date-fns formatting functions. Use date-fns for Gregorian formatting and this package for Hijri-specific tokens.
```typescript
import { format } from 'date-fns';
import { formatHijriDate } from 'date-fns-hijri';
const date = new Date(2023, 2, 23);
// Gregorian day of week from date-fns
const dayOfWeek = format(date, 'EEEE'); // 'Thursday'
// Hijri date from date-fns-hijri
const hijriLabel = formatHijriDate(date, 'iD iMMMM iYYYY'); // '1 Ramadan 1444'
const combined = `${dayOfWeek}, ${hijriLabel}`;
// 'Thursday, 1 Ramadan 1444'
```
## TypeScript: narrowing the return type
When you know the date is within the UAQ range, you can assert non-null:
```typescript
import { toHijriDate, HijriDate } from 'date-fns-hijri';
function convert(date: Date): HijriDate {
const result = toHijriDate(date);
if (result === null) throw new RangeError('Out of UAQ range');
return result;
}
```

104
.github/wiki/guides/quickstart.md vendored Normal file
View file

@ -0,0 +1,104 @@
# Quick Start
This guide covers the most common use cases in date-fns-hijri. All examples use the default Umm al-Qura (UAQ) calendar. For FCNA/ISNA calendar output, pass `{ calendar: 'fcna' }` as the last argument to any function.
## Installation
```bash
pnpm add date-fns-hijri hijri-core
```
`hijri-core` is a required peer dependency. It provides the calendar engine and must be installed alongside this package.
## Convert a Gregorian date to Hijri
```typescript
import { toHijriDate } from 'date-fns-hijri';
const date = new Date(2023, 2, 23); // March 23, 2023
const hijri = toHijriDate(date);
// { hy: 1444, hm: 9, hd: 1 }
```
`toHijriDate` returns `null` for dates outside the UAQ table range (1318-1500 AH, approximately 1900-2076 CE). Always check for null before using the result.
## Convert a Hijri date to Gregorian
```typescript
import { fromHijriDate } from 'date-fns-hijri';
const gregorian = fromHijriDate(1444, 9, 1);
// Date: 2023-03-23T00:00:00.000Z
```
## Format a Hijri date
```typescript
import { formatHijriDate } from 'date-fns-hijri';
const date = new Date(2023, 2, 23);
const label = formatHijriDate(date, 'iDD iMMMM iYYYY');
// '01 Ramadan 1444'
```
Supported format tokens:
| Token | Output |
| ------ | ----------------------- |
| `iYYYY`| Hijri year (4 digits) |
| `iYY` | Hijri year (2 digits) |
| `iMM` | Month number (01-12) |
| `iMMM` | Short month name |
| `iMMMM`| Full month name |
| `iDD` | Day (01-30) |
| `iD` | Day (1-30) |
## Get a month name
```typescript
import { getHijriMonthName } from 'date-fns-hijri';
const name = getHijriMonthName(9);
// 'Ramadan'
const shortName = getHijriMonthName(9, { format: 'short' });
// 'Ram'
```
## Add months in Hijri space
```typescript
import { addHijriMonths } from 'date-fns-hijri';
const ramadan = new Date(2023, 2, 23); // 1 Ramadan 1444
const shawwal = addHijriMonths(ramadan, 1);
// Date representing 1 Shawwal 1444 (April 21, 2023)
```
Month arithmetic respects variable-length Hijri months (29 or 30 days depending on the calendar).
## Use the FCNA calendar
```typescript
import { toHijriDate, formatHijriDate } from 'date-fns-hijri';
const opts = { calendar: 'fcna' };
const hijri = toHijriDate(new Date(2023, 2, 23), opts);
const label = formatHijriDate(new Date(2023, 2, 23), 'iDD iMMMM iYYYY', opts);
```
FCNA (Fiqh Council of North America) uses astronomical new moon calculation rather than the Umm al-Qura table. Results may differ by one day around month boundaries.
## CommonJS
```js
const { toHijriDate, fromHijriDate, formatHijriDate } = require('date-fns-hijri');
const hijri = toHijriDate(new Date(2023, 2, 23));
```
## Next steps
- [API Reference](API-Reference) for the full function list and signatures
- [Architecture](Architecture) for how the calendar engine and format layer work
- [Advanced Guide](guides/advanced) for error handling, range validation, and locale patterns

View file

@ -15,11 +15,12 @@ jobs:
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
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: node --test test.mjs
@ -30,11 +31,12 @@ jobs:
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
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm run format:check
@ -44,11 +46,12 @@ jobs:
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
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
@ -57,11 +60,12 @@ jobs:
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
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Verify pack contents