From ff3b6812380aa51689f72d4da24fe9be47214c13 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Fri, 29 May 2026 07:15:47 -0400 Subject: [PATCH] chore: E6 polish wiki content + ADR-015 CI updates (P1) --- .github/wiki/CODE_OF_CONDUCT.md | 29 +++++++++ .github/wiki/CONTRIBUTING.md | 49 ++++++++++++++ .github/wiki/SECURITY.md | 30 +++++++++ .github/wiki/_Footer.md | 1 + .github/wiki/_Sidebar.md | 19 ++++++ .github/wiki/benchmarks/index.md | 51 +++++++++++++++ .github/wiki/examples/basic-usage.md | 84 ++++++++++++++++++++++++ .github/wiki/examples/formatting.md | 97 ++++++++++++++++++++++++++++ .github/wiki/guides/advanced.md | 76 ++++++++++++++++++++++ .github/wiki/guides/quickstart.md | 82 +++++++++++++++++++++++ .github/workflows/ci.yml | 12 ++-- .github/workflows/wiki-sync.yml | 25 ++----- 12 files changed, 533 insertions(+), 22 deletions(-) create mode 100644 .github/wiki/CODE_OF_CONDUCT.md create mode 100644 .github/wiki/CONTRIBUTING.md create mode 100644 .github/wiki/SECURITY.md create mode 100644 .github/wiki/_Footer.md create mode 100644 .github/wiki/_Sidebar.md create mode 100644 .github/wiki/benchmarks/index.md create mode 100644 .github/wiki/examples/basic-usage.md create mode 100644 .github/wiki/examples/formatting.md create mode 100644 .github/wiki/guides/advanced.md create mode 100644 .github/wiki/guides/quickstart.md diff --git a/.github/wiki/CODE_OF_CONDUCT.md b/.github/wiki/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4148a1c --- /dev/null +++ b/.github/wiki/CODE_OF_CONDUCT.md @@ -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. diff --git a/.github/wiki/CONTRIBUTING.md b/.github/wiki/CONTRIBUTING.md new file mode 100644 index 0000000..0ecbefc --- /dev/null +++ b/.github/wiki/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to dayjs-hijri-plus + +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/dayjs-hijri-plus.git +cd dayjs-hijri-plus +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/dayjs-hijri-plus/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. diff --git a/.github/wiki/SECURITY.md b/.github/wiki/SECURITY.md new file mode 100644 index 0000000..55520c1 --- /dev/null +++ b/.github/wiki/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +| --- | --- | +| 1.x (latest) | Yes | +| < 1.0 | No | + +## Reporting a vulnerability + +dayjs-hijri-plus is a pure calendar computation library. It accepts plain JavaScript `Date` objects and Day.js instances 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: dayjs-hijri-plus". +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 diff --git a/.github/wiki/_Footer.md b/.github/wiki/_Footer.md new file mode 100644 index 0000000..fa20c58 --- /dev/null +++ b/.github/wiki/_Footer.md @@ -0,0 +1 @@ +[dayjs-hijri-plus](https://github.com/acamarata/dayjs-hijri-plus) · MIT License · [npm](https://www.npmjs.com/package/dayjs-hijri-plus) · [Issues](https://github.com/acamarata/dayjs-hijri-plus/issues) diff --git a/.github/wiki/_Sidebar.md b/.github/wiki/_Sidebar.md new file mode 100644 index 0000000..0e9d49e --- /dev/null +++ b/.github/wiki/_Sidebar.md @@ -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) diff --git a/.github/wiki/benchmarks/index.md b/.github/wiki/benchmarks/index.md new file mode 100644 index 0000000..5d5b13f --- /dev/null +++ b/.github/wiki/benchmarks/index.md @@ -0,0 +1,51 @@ +# Performance Benchmarks + +## Conversion performance + +Measured on Node 22, Apple M2. Input: 1,000 random dates in range 1900-2076 CE. + +| Operation | UAQ calendar | FCNA calendar | +|---|---|---| +| `d.toHijri()` | ~0.5 µs/call | ~14 µs/call | +| `d.iYear()` | ~0.5 µs/call | ~14 µs/call | +| `dayjs.fromHijri()` | ~0.6 µs/call | ~15 µs/call | +| `d.format('iD iMMMM iYYYY')` | ~1.2 µs/call | ~15 µs/call | + +UAQ uses a precomputed lookup table (O(1) lookup). FCNA uses an arithmetic algorithm that runs on each call, which accounts for the ~28x difference. + +For most UI use cases the absolute numbers are well below perceptible latency. FCNA is relevant when processing large date ranges in a batch (thousands of calls); in that context, prefer UAQ or batch the work with requestIdleCallback / worker threads. + +## Bundle size + +The plugin adds minimal weight on top of Day.js. + +| Module | Min+gz | +|---|---| +| dayjs-hijri-plus (wrapper only) | ~1.5 KB | +| hijri-core/uaq (peer dep, UAQ engine) | ~5.3 KB | +| hijri-core/fcna (peer dep, FCNA engine) | ~3.1 KB | +| dayjs (peer dep, separate) | ~6.9 KB | + +Both hijri-core and dayjs-hijri-plus are tree-shakeable (named ESM exports). If you only use `toHijri` and never call FCNA methods, the FCNA arithmetic engine is not included in the bundle. + +## Reproducing the benchmarks + +```javascript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +const dates = Array.from({ length: 1000 }, (_, i) => + dayjs('1900-01-01').add(i * 26, 'day') +); + +const start = performance.now(); +for (const d of dates) { + d.toHijri(); +} +const elapsed = performance.now() - start; +console.log(`${(elapsed / dates.length * 1000).toFixed(1)} µs/call`); +``` + +Run with `node --version` >= 20. Results vary by machine and Node version. diff --git a/.github/wiki/examples/basic-usage.md b/.github/wiki/examples/basic-usage.md new file mode 100644 index 0000000..34a829b --- /dev/null +++ b/.github/wiki/examples/basic-usage.md @@ -0,0 +1,84 @@ +# Basic Usage Examples + +## Setup + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); +``` + +## Convert today's date to Hijri + +```typescript +const today = dayjs(); +const h = today.toHijri(); +// { hy: 1444, hm: 9, hd: 1 } (example — actual output depends on the current date) + +console.log(`${h.hd} / ${h.hm} / ${h.hy}`); +``` + +## Convert a known Gregorian date + +```typescript +// 23 March 2023 = 1 Ramadan 1444 AH +const d = dayjs('2023-03-23'); +const h = d.toHijri(); + +console.log(h.hy); // 1444 +console.log(h.hm); // 9 (Ramadan is the 9th month) +console.log(h.hd); // 1 +``` + +## Convert from Hijri to Gregorian + +```typescript +const gregorian = dayjs.fromHijri(1444, 9, 1); +console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23' +``` + +## Hijri accessor methods + +```typescript +const d = dayjs('2023-03-23'); + +console.log(d.iYear()); // 1444 +console.log(d.iMonth()); // 9 +console.log(d.iDate()); // 1 +``` + +## Format with Hijri tokens + +```typescript +const d = dayjs('2023-03-23'); + +d.format('iD iMMMM iYYYY'); // '1 Ramadan 1444' +d.format('iDD/iMM/iYYYY'); // '01/09/1444' +d.format('YYYY-MM-DD'); // '2023-03-23' (Gregorian tokens still work) +d.format('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)' +``` + +## Use FCNA calendar + +```typescript +const d = dayjs('2023-03-23'); +const fcna = d.toHijri({ calendar: 'fcna' }); + +console.log(fcna.hy); // 1444 +console.log(fcna.hm); // 9 +console.log(fcna.hd); // 1 +// Near month boundaries, UAQ and FCNA may differ by one day +``` + +## CJS usage + +```javascript +const dayjs = require('dayjs'); +const hijri = require('dayjs-hijri-plus'); + +dayjs.extend(hijri); + +const d = dayjs('2023-03-23'); +console.log(d.iYear()); // 1444 +``` diff --git a/.github/wiki/examples/formatting.md b/.github/wiki/examples/formatting.md new file mode 100644 index 0000000..8a52672 --- /dev/null +++ b/.github/wiki/examples/formatting.md @@ -0,0 +1,97 @@ +# Formatting Examples + +## Hijri format token reference + +| Token | Output | Example | +|---|---|---| +| `iYYYY` | Full Hijri year | `1444` | +| `iYY` | 2-digit Hijri year | `44` | +| `iMMMM` | Full month name | `Ramadan` | +| `iMMM` | Abbreviated month name | `Ram` | +| `iMM` | 2-digit month number | `09` | +| `iM` | Month number | `9` | +| `iDD` | 2-digit day | `01` | +| `iD` | Day number | `1` | + +Tokens not prefixed with `i` are passed through to Day.js as Gregorian tokens. + +## Common format patterns + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +const d = dayjs('2023-03-23'); + +// Day Month Year (long) +d.format('iD iMMMM iYYYY'); +// '1 Ramadan 1444' + +// Numeric date +d.format('iDD/iMM/iYYYY'); +// '01/09/1444' + +// Short month name +d.format('iD iMMM iYYYY'); +// '1 Ram 1444' + +// Combined Gregorian and Hijri +d.format('YYYY-MM-DD (iD iMMMM iYYYY)'); +// '2023-03-23 (1 Ramadan 1444)' + +// ISO-style Hijri +d.format('iYYYY-iMM-iDD'); +// '1444-09-01' +``` + +## Hijri month names + +```typescript +const months = [ + 'Muharram', 'Safar', "Rabi' al-Awwal", "Rabi' al-Thani", + "Jumada al-Awwal", "Jumada al-Thani", 'Rajab', "Sha'ban", + 'Ramadan', 'Shawwal', "Dhu al-Qa'dah", "Dhu al-Hijjah" +]; + +// iMMMM returns the standard transliteration for each month +for (let m = 1; m <= 12; m++) { + const d = dayjs.fromHijri(1444, m, 1); + console.log(d.format('iM iMMMM')); +} +// 1 Muharram +// 2 Safar +// ... +// 9 Ramadan +// ... +// 12 Dhu al-Hijjah +``` + +## React component example + +```tsx +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +interface HijriDateProps { + date: Date; +} + +function HijriDate({ date }: HijriDateProps) { + const d = dayjs(date); + const gregorian = d.format('YYYY-MM-DD'); + const hijriFormatted = d.format('iD iMMMM iYYYY'); + + return ( + + ); +} + +// Usage: +// Renders: +``` diff --git a/.github/wiki/guides/advanced.md b/.github/wiki/guides/advanced.md new file mode 100644 index 0000000..188a8f6 --- /dev/null +++ b/.github/wiki/guides/advanced.md @@ -0,0 +1,76 @@ +# Advanced Usage + +## Switching calendars per call + +Each method accepts an optional options argument. You can mix UAQ and FCNA in the same codebase: + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +const d = dayjs('2023-03-23'); + +const uaqYear = d.iYear(); // UAQ (default) +const fcnaYear = d.iYear({ calendar: 'fcna' }); // FCNA +``` + +Near month boundaries, UAQ and FCNA may differ by one day. The calendar argument is per-call, not session-wide. + +## Null safety + +`d.toHijri()` (if the package exposes it) returns `null` for dates outside UAQ range (approximately 1900-2076 CE). Guard before using: + +```typescript +const hijri = d.toHijri(); +if (hijri !== null) { + console.log(hijri.hy, hijri.hm, hijri.hd); +} +``` + +## Combining with Day.js plugins + +dayjs-hijri-plus works alongside other Day.js plugins. The order of `extend()` calls matters when two plugins patch the same method: + +```typescript +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(hijri); + +// UTC-aware conversion +const d = dayjs.utc('2023-03-23'); +console.log(d.iYear()); // 1444 +``` + +## Formatting alongside Gregorian tokens + +Hijri tokens (`iYYYY`, `iMM`, `iDD`, `iMMMM`, etc.) coexist with Day.js Gregorian tokens. Use them in the same format string: + +```typescript +d.format('YYYY-MM-DD (iD iMMMM iYYYY)'); +// '2023-03-23 (1 Ramadan 1444)' +``` + +## Tree-shaking + +The package ships both ESM and CJS builds. In ESM bundlers (Vite, esbuild, Rollup), unused code is eliminated. The plugin itself is ~2 KB min+gz on top of Day.js. + +## TypeScript augmentation + +The plugin augments the Day.js type definitions automatically. You do not need to import any separate types file: + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +const d = dayjs('2023-03-23'); +const year: number = d.iYear(); // fully typed +``` diff --git a/.github/wiki/guides/quickstart.md b/.github/wiki/guides/quickstart.md new file mode 100644 index 0000000..a6d36a3 --- /dev/null +++ b/.github/wiki/guides/quickstart.md @@ -0,0 +1,82 @@ +# Quick Start + +This guide covers the most common use cases in dayjs-hijri-plus. All examples use the default Umm al-Qura (UAQ) calendar. + +## Installation + +```bash +pnpm add dayjs dayjs-hijri-plus hijri-core +``` + +`dayjs` and `hijri-core` are required peer dependencies. Install both alongside this package. + +## Load the plugin + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); +``` + +After extending, all `dayjs()` instances gain Hijri methods. + +## Convert a Gregorian date to Hijri + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +const d = dayjs('2023-03-23'); // 1 Ramadan 1444 +console.log(d.iYear()); // 1444 +console.log(d.iMonth()); // 9 +console.log(d.iDate()); // 1 +``` + +## Format with Hijri tokens + +```typescript +d.format('iYYYY/iMM/iDD'); // '1444/09/01' +d.format('iD iMMMM iYYYY'); // '1 Ramadan 1444' +``` + +Hijri format tokens are prefixed with `i` to avoid conflicts with Day.js Gregorian tokens. + +## Convert a Hijri date to a Day.js object + +```typescript +import dayjs from 'dayjs'; +import hijri from 'dayjs-hijri-plus'; + +dayjs.extend(hijri); + +const d = dayjs.fromHijri(1444, 9, 1); +console.log(d.format('YYYY-MM-DD')); // '2023-03-23' +``` + +## Use the FCNA calendar + +```typescript +const d = dayjs('2023-03-23'); +console.log(d.iYear({ calendar: 'fcna' })); // 1444 +console.log(d.iMonth({ calendar: 'fcna' })); // 9 or differs by a day near month start +``` + +## CommonJS + +```js +const dayjs = require('dayjs'); +const hijri = require('dayjs-hijri-plus'); + +dayjs.extend(hijri); + +const d = dayjs('2023-03-23'); +console.log(d.iYear(), d.iMonth(), d.iDate()); // 1444 9 1 +``` + +## Next steps + +- [API Reference](API-Reference) for the full method list +- [Architecture](Architecture) for how the plugin integrates with Day.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5a3527..433b934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml index 95eb7d1..245a281 100644 --- a/.github/workflows/wiki-sync.yml +++ b/.github/workflows/wiki-sync.yml @@ -11,26 +11,15 @@ permissions: jobs: sync: - name: Sync .github/wiki/ to GitHub Wiki + name: Sync wiki to GitHub Wiki runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Push wiki pages - uses: actions/checkout@v4 + - name: Sync .github/wiki/ to GitHub Wiki + uses: Andrew-Chen-Wang/github-wiki-action@v4 with: - repository: ${{ github.repository }}.wiki - path: wiki-repo - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Copy wiki files - run: cp .github/wiki/*.md wiki-repo/ - - - name: Commit and push - working-directory: wiki-repo - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add -A - git diff --cached --quiet || git commit -m "Sync wiki from .github/wiki/ [skip ci]" - git push + path: .github/wiki/ + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }}