mirror of
https://github.com/acamarata/moon-cycle.git
synced 2026-07-02 19:50:42 +00:00
Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09079091ae | ||
|
|
a62239c019 | ||
|
|
49d880b751 | ||
|
|
2d18b68794 | ||
|
|
2908ae7fa0 | ||
|
|
208f7ffba1 | ||
|
|
800a74670e | ||
|
|
866ea69ce2 | ||
|
|
0284076ea0 | ||
|
|
53ff3db716 | ||
|
|
8e1d53a440 | ||
|
|
130d025e63 | ||
|
|
bfcdc91411 | ||
|
|
bf92d67a56 | ||
|
|
0744ae0080 | ||
|
|
c80a139d4f | ||
|
|
9b9abb99c8 |
49 changed files with 3100 additions and 694 deletions
138
.forgejo/workflows/ci.yml
Normal file
138
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Forgejo CI mirror — git.ariccamarata.com
|
||||
# Mirrors .github/workflows/ci.yml for the self-hosted Forgejo Actions runner.
|
||||
# Keep in sync with the GitHub workflow; only addition is the nSentry failure step.
|
||||
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
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: node --test test.mjs
|
||||
- run: node --test test-cjs.cjs
|
||||
- name: Report failure to nSentry
|
||||
if: failure()
|
||||
run: |
|
||||
# nself sentry ci enable <repo> must be run on the CamClaw server first.
|
||||
# Once registered, the runner's nself-sentry-sync hook delivers this report
|
||||
# to ~/Sites/acamarata/.claude/inbox via root@sentry-errors.ariccamarata.com.
|
||||
echo "CI_FAILURE repo=${{ github.repository }} job=${{ github.job }} run=${{ github.run_id }}" >&2
|
||||
|
||||
lint:
|
||||
name: Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run format:check
|
||||
- name: Report failure to nSentry
|
||||
if: failure()
|
||||
run: |
|
||||
# nself sentry ci enable <repo> must be run on the CamClaw server first.
|
||||
# Once registered, the runner's nself-sentry-sync hook delivers this report
|
||||
# to ~/Sites/acamarata/.claude/inbox via root@sentry-errors.ariccamarata.com.
|
||||
echo "CI_FAILURE repo=${{ github.repository }} job=${{ github.job }} run=${{ github.run_id }}" >&2
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run typecheck
|
||||
- name: Report failure to nSentry
|
||||
if: failure()
|
||||
run: |
|
||||
# nself sentry ci enable <repo> must be run on the CamClaw server first.
|
||||
# Once registered, the runner's nself-sentry-sync hook delivers this report
|
||||
# to ~/Sites/acamarata/.claude/inbox via root@sentry-errors.ariccamarata.com.
|
||||
echo "CI_FAILURE repo=${{ github.repository }} job=${{ github.job }} run=${{ github.run_id }}" >&2
|
||||
|
||||
pack-check:
|
||||
name: Pack check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Verify pack contents
|
||||
run: |
|
||||
PACK=$(npm pack --dry-run 2>&1)
|
||||
echo "$PACK"
|
||||
for f in dist/index.cjs dist/index.mjs dist/index.d.ts dist/index.d.mts README.md LICENSE CHANGELOG.md; do
|
||||
echo "$PACK" | grep -q "$f" || (echo "Missing: $f" && exit 1)
|
||||
done
|
||||
echo "$PACK" | grep -q "src/" && (echo "ERROR: src/ should not be in pack" && exit 1) || true
|
||||
echo "Pack check passed"
|
||||
- name: Report failure to nSentry
|
||||
if: failure()
|
||||
run: |
|
||||
# nself sentry ci enable <repo> must be run on the CamClaw server first.
|
||||
# Once registered, the runner's nself-sentry-sync hook delivers this report
|
||||
# to ~/Sites/acamarata/.claude/inbox via root@sentry-errors.ariccamarata.com.
|
||||
echo "CI_FAILURE repo=${{ github.repository }} job=${{ github.job }} run=${{ github.run_id }}" >&2
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm run coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
- name: Report failure to nSentry
|
||||
if: failure()
|
||||
run: |
|
||||
# nself sentry ci enable <repo> must be run on the CamClaw server first.
|
||||
# Once registered, the runner's nself-sentry-sync hook delivers this report
|
||||
# to ~/Sites/acamarata/.claude/inbox via root@sentry-errors.ariccamarata.com.
|
||||
echo "CI_FAILURE repo=${{ github.repository }} job=${{ github.job }} run=${{ github.run_id }}" >&2
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
github: [acamarata]
|
||||
45
.github/docs/CHANGELOG.md
vendored
Normal file
45
.github/docs/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [2.0.0] - 2025-02-25
|
||||
|
||||
### Added
|
||||
|
||||
- TypeScript source (`src/`) with full type definitions: dual CJS and ESM builds via tsup
|
||||
- `imageFolder(set, size, quality)` helper to construct image directory names
|
||||
- `cdnUrl(filename, set, size, quality, ref?)` helper to generate jsDelivr CDN URLs, enabling image serving without self-hosting the ~438 MB dataset
|
||||
- Exported constants: `SYNODIC_MONTH`, `MONTH_IMAGES`, `YEAR_IMAGES`, `MONTH_ANCHOR`, `YEAR_ANCHOR`
|
||||
- Exported types: `ImageSet`, `ImageSize`, `ImageQuality`
|
||||
- Dual ESM and CJS builds with `.mjs` / `.cjs` extensions and matching `.d.ts` / `.d.mts` type definitions
|
||||
- Proper `exports` map in `package.json` (types-first conditional exports)
|
||||
- `test.mjs` and `test-cjs.cjs`: full assertion-based test suites covering bounds, anchor dates, edge cases, and all exported functions
|
||||
- GitHub Actions CI workflow: Node 20/22/24 test matrix, typecheck job, pack-check job
|
||||
- GitHub Actions wiki-sync workflow: syncs `.wiki/` to GitHub Wiki on push to `main`
|
||||
- `.wiki/` documentation: Home, API Reference, Architecture, Migration Guide
|
||||
- `.nvmrc`, `.editorconfig`, `.npmrc`, `pnpm-workspace.yaml`
|
||||
|
||||
### Changed
|
||||
|
||||
- Package is now npm-publishable: `files` field restricts the npm package to `dist/`, README, LICENSE, and CHANGELOG: images are excluded
|
||||
- `package.json` fully updated: correct author name, accurate description, `engines`, `sideEffects`, `publishConfig`, `repository.url` with `git+https://` prefix, expanded keywords
|
||||
- `repository.url` corrected to use `git+https://` prefix per npm convention
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Off-by-one bug in both algorithms.** The v1 implementation mapped dates to 0-indexed filenames (`000.webp` to `707.webp` monthly, `0000.webp` to `8759.webp` yearly). The image dataset is 1-indexed (`001.webp` to `708.webp`, `0001.webp` to `8760.webp`). Both functions now return the correct 1-indexed filename. This is a breaking change for anyone who was working around the bug or had a local image set starting at `000.webp`.
|
||||
- TypeScript definitions in `index.d.ts` were incorrect: both functions were typed as returning `{ result: string }` instead of `string`. The new generated types are accurate.
|
||||
|
||||
### Removed
|
||||
|
||||
- `index.js`, `cycleMonth.js`, `cycleYear.js`: replaced by `src/` TypeScript source and `dist/` build output
|
||||
- `index.d.ts`: replaced by generated `dist/index.d.ts` and `dist/index.d.mts`
|
||||
- `test.js`: replaced by `test.mjs` and `test-cjs.cjs`
|
||||
|
||||
## [1.0.1] - 2023-11-14
|
||||
|
||||
- Minor repository metadata updates
|
||||
|
||||
## [1.0.0] - 2023-11-14
|
||||
|
||||
- Initial release: `cycleMonth` and `cycleYear` functions, 708 monthly and 8,760 yearly NASA moon phase images in WebP format
|
||||
|
|
@ -7,7 +7,7 @@ Complete reference for moon-cycle v2.
|
|||
### `cycleMonth(date?)`
|
||||
|
||||
```ts
|
||||
function cycleMonth(date?: Date): string
|
||||
function cycleMonth(date?: Date): string;
|
||||
```
|
||||
|
||||
Maps a date to an image filename in the **monthly (synodic) dataset**.
|
||||
|
|
@ -16,8 +16,8 @@ The 708 images cover one complete synodic month at hourly resolution. The functi
|
|||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| Name | Type | Default | Description |
|
||||
| ------ | ------ | ------------ | ------------------- |
|
||||
| `date` | `Date` | `new Date()` | The date to resolve |
|
||||
|
||||
**Returns:** A zero-padded filename string, e.g. `"354.webp"`. Always in the range `"001.webp"` to `"708.webp"`.
|
||||
|
|
@ -27,9 +27,9 @@ The 708 images cover one complete synodic month at hourly resolution. The functi
|
|||
```ts
|
||||
import { cycleMonth } from 'moon-cycle';
|
||||
|
||||
cycleMonth(); // current lunar phase
|
||||
cycleMonth(new Date('2024-01-15')); // specific date
|
||||
cycleMonth(new Date('2020-06-21')); // any past date works
|
||||
cycleMonth(); // current lunar phase
|
||||
cycleMonth(new Date('2024-01-15')); // specific date
|
||||
cycleMonth(new Date('2020-06-21')); // any past date works
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -37,7 +37,7 @@ cycleMonth(new Date('2020-06-21')); // any past date works
|
|||
### `cycleYear(date?)`
|
||||
|
||||
```ts
|
||||
function cycleYear(date?: Date): string
|
||||
function cycleYear(date?: Date): string;
|
||||
```
|
||||
|
||||
Maps a date to an image filename in the **yearly dataset**.
|
||||
|
|
@ -46,8 +46,8 @@ The 8,760 images cover the full calendar year 2023 at hourly resolution. The fun
|
|||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| Name | Type | Default | Description |
|
||||
| ------ | ------ | ------------ | ------------------- |
|
||||
| `date` | `Date` | `new Date()` | The date to resolve |
|
||||
|
||||
**Returns:** A zero-padded filename string, e.g. `"4380.webp"`. Always in the range `"0001.webp"` to `"8760.webp"`.
|
||||
|
|
@ -57,7 +57,7 @@ The 8,760 images cover the full calendar year 2023 at hourly resolution. The fun
|
|||
```ts
|
||||
import { cycleYear } from 'moon-cycle';
|
||||
|
||||
cycleYear(); // current hour-of-year position
|
||||
cycleYear(); // current hour-of-year position
|
||||
cycleYear(new Date('2025-07-04T12:00Z')); // July 4, noon
|
||||
```
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ cycleYear(new Date('2025-07-04T12:00Z')); // July 4, noon
|
|||
### `imageFolder(set, size, quality)`
|
||||
|
||||
```ts
|
||||
function imageFolder(set: ImageSet, size: ImageSize, quality: ImageQuality): string
|
||||
function imageFolder(set: ImageSet, size: ImageSize, quality: ImageQuality): string;
|
||||
```
|
||||
|
||||
Returns the directory name for a given combination of image set, size, and quality.
|
||||
|
|
@ -75,11 +75,11 @@ Directory names follow the pattern `{set}-{size}-{quality}`, matching the layout
|
|||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `set` | `'mm' \| 'my'` | Monthly or yearly dataset |
|
||||
| `size` | `256 \| 512` | Image dimension in pixels |
|
||||
| `quality` | `75 \| 85` | WebP compression quality |
|
||||
| Name | Type | Description |
|
||||
| --------- | -------------- | ------------------------- |
|
||||
| `set` | `'mm' \| 'my'` | Monthly or yearly dataset |
|
||||
| `size` | `256 \| 512` | Image dimension in pixels |
|
||||
| `quality` | `75 \| 85` | WebP compression quality |
|
||||
|
||||
**Returns:** A string like `"mm-256-75"`.
|
||||
|
||||
|
|
@ -88,12 +88,12 @@ Directory names follow the pattern `{set}-{size}-{quality}`, matching the layout
|
|||
```ts
|
||||
import { imageFolder } from 'moon-cycle';
|
||||
|
||||
imageFolder('mm', 256, 75) // => 'mm-256-75'
|
||||
imageFolder('my', 512, 85) // => 'my-512-85'
|
||||
imageFolder('mm', 256, 75); // => 'mm-256-75'
|
||||
imageFolder('my', 512, 85); // => 'my-512-85'
|
||||
|
||||
// Use with a local public directory:
|
||||
const filename = cycleMonth();
|
||||
const folder = imageFolder('mm', 512, 85);
|
||||
const folder = imageFolder('mm', 512, 85);
|
||||
const localUrl = `/public/${folder}/${filename}`;
|
||||
```
|
||||
|
||||
|
|
@ -107,21 +107,21 @@ function cdnUrl(
|
|||
set: ImageSet,
|
||||
size: ImageSize,
|
||||
quality: ImageQuality,
|
||||
ref?: string
|
||||
): string
|
||||
ref?: string,
|
||||
): string;
|
||||
```
|
||||
|
||||
Returns a jsDelivr CDN URL serving the specified image directly from the GitHub repository. jsDelivr caches GitHub content via a global CDN with no account or configuration required.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `filename` | `string` | — | Filename from `cycleMonth()` or `cycleYear()` |
|
||||
| `set` | `'mm' \| 'my'` | — | Monthly or yearly dataset |
|
||||
| `size` | `256 \| 512` | — | Image dimension in pixels |
|
||||
| `quality` | `75 \| 85` | — | WebP compression quality |
|
||||
| `ref` | `string` | `'main'` | Branch name, git tag, or commit SHA |
|
||||
| Name | Type | Default | Description |
|
||||
| ---------- | -------------- | -------- | --------------------------------------------- |
|
||||
| `filename` | `string` | : | Filename from `cycleMonth()` or `cycleYear()` |
|
||||
| `set` | `'mm' \| 'my'` | : | Monthly or yearly dataset |
|
||||
| `size` | `256 \| 512` | : | Image dimension in pixels |
|
||||
| `quality` | `75 \| 85` | : | WebP compression quality |
|
||||
| `ref` | `string` | `'main'` | Branch name, git tag, or commit SHA |
|
||||
|
||||
**Returns:** A full URL string, e.g. `"https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp"`.
|
||||
|
||||
|
|
@ -148,13 +148,13 @@ function MoonImage() {
|
|||
|
||||
## Constants
|
||||
|
||||
| Export | Type | Value | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `SYNODIC_MONTH` | `number` | `29.53058821398858` | IAU mean synodic month in days |
|
||||
| `MONTH_IMAGES` | `number` | `708` | Image count in the monthly dataset |
|
||||
| `YEAR_IMAGES` | `number` | `8760` | Image count in the yearly dataset |
|
||||
| `MONTH_ANCHOR` | `Date` | `2023-11-13T09:27:00Z` | New moon reference for `cycleMonth` |
|
||||
| `YEAR_ANCHOR` | `Date` | `2023-01-01T00:00:00Z` | Year start reference for `cycleYear` |
|
||||
| Export | Type | Value | Description |
|
||||
| --------------- | -------- | ---------------------- | ------------------------------------ |
|
||||
| `SYNODIC_MONTH` | `number` | `29.53058821398858` | IAU mean synodic month in days |
|
||||
| `MONTH_IMAGES` | `number` | `708` | Image count in the monthly dataset |
|
||||
| `YEAR_IMAGES` | `number` | `8760` | Image count in the yearly dataset |
|
||||
| `MONTH_ANCHOR` | `Date` | `2023-11-13T09:27:00Z` | New moon reference for `cycleMonth` |
|
||||
| `YEAR_ANCHOR` | `Date` | `2023-01-01T00:00:00Z` | Year start reference for `cycleYear` |
|
||||
|
||||
**Example:**
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ import { SYNODIC_MONTH, MONTH_ANCHOR } from 'moon-cycle';
|
|||
// Compute the next new moon after a given date
|
||||
function nextNewMoon(after: Date): Date {
|
||||
const elapsed = (after.getTime() - MONTH_ANCHOR.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const cycles = Math.ceil(elapsed / SYNODIC_MONTH);
|
||||
const cycles = Math.ceil(elapsed / SYNODIC_MONTH);
|
||||
return new Date(MONTH_ANCHOR.getTime() + cycles * SYNODIC_MONTH * 86400000);
|
||||
}
|
||||
```
|
||||
|
|
@ -189,19 +189,19 @@ type ImageQuality = 75 | 85;
|
|||
|
||||
## Image Dataset Reference
|
||||
|
||||
| Folder | Set | Images | Resolution | Quality | Approx. Size |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `mm-256-75` | monthly | 708 | 256x256 | 75 | ~4 MB |
|
||||
| `mm-256-85` | monthly | 708 | 256x256 | 85 | ~5 MB |
|
||||
| `mm-512-75` | monthly | 708 | 512x512 | 75 | ~9 MB |
|
||||
| `mm-512-85` | monthly | 708 | 512x512 | 85 | ~14 MB |
|
||||
| `my-256-75` | yearly | 8,760 | 256x256 | 75 | ~51 MB |
|
||||
| `my-256-85` | yearly | 8,760 | 256x256 | 85 | ~66 MB |
|
||||
| `my-512-75` | yearly | 8,760 | 512x512 | 75 | ~113 MB |
|
||||
| `my-512-85` | yearly | 8,760 | 512x512 | 85 | ~176 MB |
|
||||
| Folder | Set | Images | Resolution | Quality | Approx. Size |
|
||||
| ----------- | ------- | ------ | ---------- | ------- | ------------ |
|
||||
| `mm-256-75` | monthly | 708 | 256x256 | 75 | ~4 MB |
|
||||
| `mm-256-85` | monthly | 708 | 256x256 | 85 | ~5 MB |
|
||||
| `mm-512-75` | monthly | 708 | 512x512 | 75 | ~9 MB |
|
||||
| `mm-512-85` | monthly | 708 | 512x512 | 85 | ~14 MB |
|
||||
| `my-256-75` | yearly | 8,760 | 256x256 | 75 | ~51 MB |
|
||||
| `my-256-85` | yearly | 8,760 | 256x256 | 85 | ~66 MB |
|
||||
| `my-512-75` | yearly | 8,760 | 512x512 | 75 | ~113 MB |
|
||||
| `my-512-85` | yearly | 8,760 | 512x512 | 85 | ~176 MB |
|
||||
|
||||
All images are square WebP, transparent background, named with zero-padded indices (`001.webp` to `708.webp` for monthly, `0001.webp` to `8760.webp` for yearly).
|
||||
|
||||
---
|
||||
|
||||
*[Home](Home) | [Architecture](Architecture) | [Migration Guide](Migration)*
|
||||
_[Home](Home) | [Architecture](Architecture) | [Migration Guide](Migration)_
|
||||
|
|
@ -12,8 +12,8 @@ All images originate from NASA's Scientific Visualization Studio visualization "
|
|||
|
||||
The raw frames were converted to WebP and organized into eight folders combining two image sets, two resolutions, and two quality levels:
|
||||
|
||||
- `mm-*` — 708 images covering one synodic month (monthly set)
|
||||
- `my-*` — 8,760 images covering the full year 2023 (yearly set)
|
||||
- `mm-*`: 708 images covering one synodic month (monthly set)
|
||||
- `my-*`: 8,760 images covering the full year 2023 (yearly set)
|
||||
|
||||
Images are named with zero-padded integers starting at 1 (`001.webp` to `708.webp` for monthly, `0001.webp` to `8760.webp` for yearly).
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ Images are named with zero-padded integers starting at 1 (`001.webp` to `708.web
|
|||
|
||||
### Concept
|
||||
|
||||
The synodic month is the time between two identical lunar phases as seen from Earth — new moon to new moon. Its IAU mean value is **29.53058821398858 days**. The 708 images in the monthly set span exactly one such cycle at hourly resolution.
|
||||
The synodic month is the time between two identical lunar phases as seen from Earth: new moon to new moon. Its IAU mean value is **29.53058821398858 days**. The 708 images in the monthly set span exactly one such cycle at hourly resolution.
|
||||
|
||||
To find the correct image for any date, the algorithm:
|
||||
|
||||
|
|
@ -45,13 +45,13 @@ index = floor(fraction * 708) + 1
|
|||
|
||||
### Limitation
|
||||
|
||||
The synodic month is not exactly 29.53058821398858 days — that value is the IAU *mean* (averaged over centuries). The actual length of a given synodic month varies by up to ~7 hours depending on the Moon's position in its elliptical orbit. For dates far from the 2023 reference period, accumulated drift means the image may be off by a few frames from the true observed phase. For UI purposes, this is imperceptible.
|
||||
The synodic month is not exactly 29.53058821398858 days: that value is the IAU _mean_ (averaged over centuries). The actual length of a given synodic month varies by up to ~7 hours depending on the Moon's position in its elliptical orbit. For dates far from the 2023 reference period, accumulated drift means the image may be off by a few frames from the true observed phase. For UI purposes, this is imperceptible.
|
||||
|
||||
## Algorithm 2: Calendar Year (`cycleYear`)
|
||||
|
||||
### Concept
|
||||
|
||||
The yearly set contains 8,760 images — one per hour of the 365-day year 2023. Rather than tracking lunar phase directly, `cycleYear` maps any date to its hour-of-year equivalent and uses that to index into the 2023 imagery.
|
||||
The yearly set contains 8,760 images: one per hour of the 365-day year 2023. Rather than tracking lunar phase directly, `cycleYear` maps any date to its hour-of-year equivalent and uses that to index into the 2023 imagery.
|
||||
|
||||
The algorithm:
|
||||
|
||||
|
|
@ -75,24 +75,24 @@ index = floor(fraction * 8760) + 1
|
|||
|
||||
### Limitation
|
||||
|
||||
The year 2023 had 365 days (not a leap year), so the 8,760 images correspond exactly to one non-leap year. For years with 366 days, there is a subtle 24-image offset in the second half of the year — the hour-of-year for December 31 in a leap year wraps around slightly differently than in 2023. This is a cosmetic artifact, not a functional error.
|
||||
The year 2023 had 365 days (not a leap year), so the 8,760 images correspond exactly to one non-leap year. For years with 366 days, there is a subtle 24-image offset in the second half of the year: the hour-of-year for December 31 in a leap year wraps around slightly differently than in 2023. This is a cosmetic artifact, not a functional error.
|
||||
|
||||
## Choosing Between the Two
|
||||
|
||||
| Scenario | Recommendation |
|
||||
| --- | --- |
|
||||
| Show the actual current moon phase | `cycleMonth` |
|
||||
| Animate through a year of moon phases for a calendar app | `cycleYear` |
|
||||
| Show a consistent seasonal moon appearance | `cycleYear` |
|
||||
| Compute when the next full moon occurs | `cycleMonth` + `SYNODIC_MONTH` |
|
||||
| Display a decorative moon that changes daily | Either — `cycleYear` has smoother hourly progression |
|
||||
| Scenario | Recommendation |
|
||||
| -------------------------------------------------------- | --------------------------------------------------- |
|
||||
| Show the actual current moon phase | `cycleMonth` |
|
||||
| Animate through a year of moon phases for a calendar app | `cycleYear` |
|
||||
| Show a consistent seasonal moon appearance | `cycleYear` |
|
||||
| Compute when the next full moon occurs | `cycleMonth` + `SYNODIC_MONTH` |
|
||||
| Display a decorative moon that changes daily | Either: `cycleYear` has smoother hourly progression |
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
moon-cycle/
|
||||
├── src/
|
||||
│ ├── index.ts # Public API — re-exports all named exports
|
||||
│ ├── index.ts # Public API: re-exports all named exports
|
||||
│ ├── types.ts # Types, constants, anchor dates
|
||||
│ ├── cycleMonth.ts # Synodic algorithm
|
||||
│ ├── cycleYear.ts # Calendar-year algorithm
|
||||
|
|
@ -112,10 +112,10 @@ moon-cycle/
|
|||
|
||||
The source is TypeScript, compiled by [tsup](https://tsup.egoist.dev/) (esbuild-based). tsup produces four output files from `src/index.ts`:
|
||||
|
||||
- `dist/index.cjs` — CommonJS for `require()`
|
||||
- `dist/index.mjs` — ESM for `import`
|
||||
- `dist/index.d.ts` — type definitions for CJS consumers
|
||||
- `dist/index.d.mts` — type definitions for ESM consumers
|
||||
- `dist/index.cjs`: CommonJS for `require()`
|
||||
- `dist/index.mjs`: ESM for `import`
|
||||
- `dist/index.d.ts`: type definitions for CJS consumers
|
||||
- `dist/index.d.mts`: type definitions for ESM consumers
|
||||
|
||||
The `exports` field in `package.json` uses types-first conditional exports so TypeScript resolves the correct declaration file regardless of `moduleResolution` setting.
|
||||
|
||||
|
|
@ -131,4 +131,4 @@ The probability of hitting the exact boundary where `000.webp` would have been r
|
|||
|
||||
---
|
||||
|
||||
*[Home](Home) | [API Reference](API-Reference) | [Migration Guide](Migration)*
|
||||
_[Home](Home) | [API Reference](API-Reference) | [Migration Guide](Migration)_
|
||||
29
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
29
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal 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.
|
||||
50
.github/wiki/CONTRIBUTING.md
vendored
Normal file
50
.github/wiki/CONTRIBUTING.md
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Contributing to moon-cycle
|
||||
|
||||
Thanks for your interest in contributing. This library maps JavaScript dates to NASA moon phase images and contributions are welcome.
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
git clone https://github.com/acamarata/moon-cycle.git
|
||||
cd moon-cycle
|
||||
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/moon-cycle/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
|
||||
|
||||
- Plain JavaScript (index.js) with a TypeScript declaration file (index.d.ts). No TypeScript source.
|
||||
- Pure functions. No global state. Date is always passed explicitly.
|
||||
- Each function: one purpose.
|
||||
- Run `pnpm run format` before committing.
|
||||
- Run `pnpm run lint` before committing.
|
||||
|
||||
## Image dataset
|
||||
|
||||
The image dataset (~438 MB) is tracked in git and should not be modified. All images are NASA public domain material from the Scientific Visualization Studio. Do not add new images without updating the algorithm constants and test coverage.
|
||||
|
||||
If you think the dataset is wrong or a mapping is incorrect, open an issue with the specific date, expected image, and actual image.
|
||||
|
||||
## Tests
|
||||
|
||||
- Tests live in `test.mjs` (ESM) and `test-cjs.cjs` (CommonJS). Both must pass.
|
||||
- Use the native Node.js `node:test` runner.
|
||||
- Test specific known dates against expected filenames.
|
||||
|
||||
## 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.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your work will be licensed under MIT. Copyright remains with Aric Camarata. NASA imagery is public domain per NASA media guidelines.
|
||||
28
.wiki/Home.md → .github/wiki/Home.md
vendored
28
.wiki/Home.md → .github/wiki/Home.md
vendored
|
|
@ -8,10 +8,10 @@ Given any date, you get a filename like `354.webp` or `4380.webp` that correspon
|
|||
|
||||
Two algorithms are provided because they answer different questions:
|
||||
|
||||
| Function | Algorithm | Use when |
|
||||
| --- | --- | --- |
|
||||
| Function | Algorithm | Use when |
|
||||
| ------------ | --------------------- | -------------------------------------------- |
|
||||
| `cycleMonth` | Synodic (lunar) cycle | You want the actual lunar phase for the date |
|
||||
| `cycleYear` | Calendar year (2023) | You want a consistent annual visual rhythm |
|
||||
| `cycleYear` | Calendar year (2023) | You want a consistent annual visual rhythm |
|
||||
|
||||
See [Architecture](Architecture) for a full explanation of each approach.
|
||||
|
||||
|
|
@ -27,8 +27,8 @@ pnpm add moon-cycle
|
|||
```ts
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const file = cycleMonth(); // e.g. "354.webp" — current lunar phase
|
||||
const url = cdnUrl(file, 'mm', 256, 75);
|
||||
const file = cycleMonth(); // e.g. "354.webp": current lunar phase
|
||||
const url = cdnUrl(file, 'mm', 256, 75);
|
||||
// => 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
```
|
||||
|
||||
|
|
@ -36,20 +36,20 @@ const url = cdnUrl(file, 'mm', 256, 75);
|
|||
|
||||
Pick the folder that fits your use case:
|
||||
|
||||
| Folder | Images | Size |
|
||||
| --- | --- | --- |
|
||||
| `mm-256-75` | 708 (monthly) | ~4 MB |
|
||||
| `mm-512-85` | 708 (monthly) | ~14 MB |
|
||||
| `my-256-75` | 8,760 (yearly) | ~51 MB |
|
||||
| Folder | Images | Size |
|
||||
| ----------- | -------------- | ------- |
|
||||
| `mm-256-75` | 708 (monthly) | ~4 MB |
|
||||
| `mm-512-85` | 708 (monthly) | ~14 MB |
|
||||
| `my-256-75` | 8,760 (yearly) | ~51 MB |
|
||||
| `my-512-85` | 8,760 (yearly) | ~176 MB |
|
||||
|
||||
All eight combinations (`mm`/`my` × `256`/`512` × `75`/`85`) are available. Use `imageFolder()` to construct the directory name programmatically.
|
||||
|
||||
## Pages
|
||||
|
||||
- [API Reference](API-Reference) — full function and type documentation
|
||||
- [Architecture](Architecture) — algorithm design, dataset description, tradeoffs
|
||||
- [Migration Guide](Migration) — upgrading from v1
|
||||
- [API Reference](API-Reference): full function and type documentation
|
||||
- [Architecture](Architecture): algorithm design, dataset description, tradeoffs
|
||||
- [Migration Guide](Migration): upgrading from v1
|
||||
|
||||
## Source
|
||||
|
||||
|
|
@ -57,4 +57,4 @@ All eight combinations (`mm`/`my` × `256`/`512` × `75`/`85`) are available. Us
|
|||
|
||||
---
|
||||
|
||||
*Part of the [acamarata](https://github.com/acamarata) astronomical computing stack.*
|
||||
_Part of the [acamarata](https://github.com/acamarata) astronomical computing stack._
|
||||
24
.wiki/Migration.md → .github/wiki/Migration.md
vendored
24
.wiki/Migration.md → .github/wiki/Migration.md
vendored
|
|
@ -5,7 +5,7 @@ Upgrading from moon-cycle v1 to v2.
|
|||
## Summary of Breaking Changes
|
||||
|
||||
1. **Off-by-one fix:** `cycleMonth` and `cycleYear` now return 1-indexed filenames
|
||||
2. **No default export:** Functions are now named exports only (this was always the case — `require('moon-cycle')` still works, but `require('moon-cycle').default` does not exist)
|
||||
2. **No default export:** Functions are now named exports only (this was always the case: `require('moon-cycle')` still works, but `require('moon-cycle').default` does not exist)
|
||||
|
||||
## 1. Off-by-one correction
|
||||
|
||||
|
|
@ -24,15 +24,17 @@ v2 corrects this. The returned filenames now match the actual files in the datas
|
|||
v1 shipped a hand-written `index.d.ts` that incorrectly declared both functions as returning `{ result: string }`:
|
||||
|
||||
```ts
|
||||
// v1 — incorrect
|
||||
// v1: incorrect
|
||||
export function cycleMonth(date: Date): MonthResult;
|
||||
interface MonthResult { result: string }
|
||||
interface MonthResult {
|
||||
result: string;
|
||||
}
|
||||
```
|
||||
|
||||
The actual runtime behavior in v1 was to return a plain `string`, not an object. v2 types match the implementation:
|
||||
|
||||
```ts
|
||||
// v2 — correct
|
||||
// v2: correct
|
||||
export function cycleMonth(date?: Date): string;
|
||||
```
|
||||
|
||||
|
|
@ -52,10 +54,10 @@ const src = `/mm-256-75/${filename}`;
|
|||
|
||||
v2 adds several exports that did not exist in v1. These are additive and do not break existing code:
|
||||
|
||||
- `imageFolder(set, size, quality)` — constructs folder name strings
|
||||
- `cdnUrl(filename, set, size, quality, ref?)` — constructs jsDelivr CDN URLs
|
||||
- `SYNODIC_MONTH`, `MONTH_IMAGES`, `YEAR_IMAGES`, `MONTH_ANCHOR`, `YEAR_ANCHOR` — constants
|
||||
- `ImageSet`, `ImageSize`, `ImageQuality` — TypeScript types
|
||||
- `imageFolder(set, size, quality)`: constructs folder name strings
|
||||
- `cdnUrl(filename, set, size, quality, ref?)`: constructs jsDelivr CDN URLs
|
||||
- `SYNODIC_MONTH`, `MONTH_IMAGES`, `YEAR_IMAGES`, `MONTH_ANCHOR`, `YEAR_ANCHOR`: constants
|
||||
- `ImageSet`, `ImageSize`, `ImageQuality`: TypeScript types
|
||||
|
||||
## 4. Optional `date` parameter
|
||||
|
||||
|
|
@ -68,11 +70,11 @@ v2 ships dual CJS and ESM builds. If you use a bundler, it will now automaticall
|
|||
## Migration Checklist
|
||||
|
||||
- [ ] Update to `moon-cycle@2.0.0`
|
||||
- [ ] Verify image filenames — if you referenced `000.webp` or `0000.webp` directly, rename to `001.webp` / `0001.webp`
|
||||
- [ ] Remove any `.result` property access — both functions return `string` directly
|
||||
- [ ] Verify image filenames: if you referenced `000.webp` or `0000.webp` directly, rename to `001.webp` / `0001.webp`
|
||||
- [ ] Remove any `.result` property access: both functions return `string` directly
|
||||
- [ ] Update TypeScript types if you had manual overrides working around the incorrect v1 declarations
|
||||
- [ ] Consider using `cdnUrl()` if you were constructing CDN URLs manually
|
||||
|
||||
---
|
||||
|
||||
*[Home](Home) | [API Reference](API-Reference) | [Architecture](Architecture)*
|
||||
_[Home](Home) | [API Reference](API-Reference) | [Architecture](Architecture)_
|
||||
29
.github/wiki/SECURITY.md
vendored
Normal file
29
.github/wiki/SECURITY.md
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
| Version | Supported |
|
||||
| --- | --- |
|
||||
| 2.x (latest) | Yes |
|
||||
| < 2.0 | No |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
moon-cycle is a pure date-to-filename mapping library. It accepts JavaScript `Date` objects as input and returns image filenames or CDN URLs. There is no network access, no file system access (image paths are resolved client-side), 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: moon-cycle".
|
||||
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
|
||||
- Prototype pollution via user-provided inputs
|
||||
|
||||
## What does not count
|
||||
|
||||
- Incorrect moon phase image selection (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
1
.github/wiki/_Footer.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[moon-cycle](https://github.com/acamarata/moon-cycle) · MIT License · [GitHub](https://github.com/acamarata/moon-cycle) · [Issues](https://github.com/acamarata/moon-cycle/issues)
|
||||
28
.github/wiki/_Sidebar.md
vendored
Normal file
28
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
**[Home](Home)**
|
||||
|
||||
**Guides**
|
||||
- [Quick Start](guides/quickstart)
|
||||
- [Advanced Usage](guides/advanced)
|
||||
|
||||
**Examples**
|
||||
- [Basic Usage](examples/basic-usage)
|
||||
- [Prayer App Integration](examples/prayer-app)
|
||||
|
||||
**API**
|
||||
- [cycleMonth](api/cycleMonth)
|
||||
- [cycleYear](api/cycleYear)
|
||||
- [cdnUrl](api/cdnUrl)
|
||||
- [imageFolder](api/imageFolder)
|
||||
- [Constants](api/constants)
|
||||
- [Types](api/types)
|
||||
|
||||
**Reference**
|
||||
- [API Reference](API-Reference)
|
||||
- [Architecture](Architecture)
|
||||
- [Migration](Migration)
|
||||
- [Benchmarks](benchmarks/index)
|
||||
|
||||
**Community**
|
||||
- [Contributing](CONTRIBUTING)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||
- [Security](SECURITY)
|
||||
26
.github/wiki/api/README.md
vendored
Normal file
26
.github/wiki/api/README.md
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
**moon-cycle v2.0.0**
|
||||
|
||||
***
|
||||
|
||||
# moon-cycle v2.0.0
|
||||
|
||||
## Type Aliases
|
||||
|
||||
- [ImageQuality](type-aliases/ImageQuality.md)
|
||||
- [ImageSet](type-aliases/ImageSet.md)
|
||||
- [ImageSize](type-aliases/ImageSize.md)
|
||||
|
||||
## Variables
|
||||
|
||||
- [MONTH\_ANCHOR](variables/MONTH_ANCHOR.md)
|
||||
- [MONTH\_IMAGES](variables/MONTH_IMAGES.md)
|
||||
- [SYNODIC\_MONTH](variables/SYNODIC_MONTH.md)
|
||||
- [YEAR\_ANCHOR](variables/YEAR_ANCHOR.md)
|
||||
- [YEAR\_IMAGES](variables/YEAR_IMAGES.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [cdnUrl](functions/cdnUrl.md)
|
||||
- [cycleMonth](functions/cycleMonth.md)
|
||||
- [cycleYear](functions/cycleYear.md)
|
||||
- [imageFolder](functions/imageFolder.md)
|
||||
63
.github/wiki/api/functions/cdnUrl.md
vendored
Normal file
63
.github/wiki/api/functions/cdnUrl.md
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / cdnUrl
|
||||
|
||||
# Function: cdnUrl()
|
||||
|
||||
> **cdnUrl**(`filename`, `set`, `size`, `quality`, `ref?`): `string`
|
||||
|
||||
Defined in: [helpers.ts:36](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/helpers.ts#L36)
|
||||
|
||||
Returns a jsDelivr CDN URL for a specific moon image.
|
||||
|
||||
jsDelivr serves files directly from GitHub repositories, making it a
|
||||
practical option for web applications that need the images without
|
||||
bundling ~438 MB of assets locally.
|
||||
|
||||
## Parameters
|
||||
|
||||
### filename
|
||||
|
||||
`string`
|
||||
|
||||
Filename returned by `cycleMonth` or `cycleYear`, e.g. `"354.webp"`.
|
||||
|
||||
### set
|
||||
|
||||
[`ImageSet`](../type-aliases/ImageSet.md)
|
||||
|
||||
Image set: `'mm'` (monthly) or `'my'` (yearly).
|
||||
|
||||
### size
|
||||
|
||||
[`ImageSize`](../type-aliases/ImageSize.md)
|
||||
|
||||
Image dimension: `256` or `512`.
|
||||
|
||||
### quality
|
||||
|
||||
[`ImageQuality`](../type-aliases/ImageQuality.md)
|
||||
|
||||
WebP quality: `75` or `85`.
|
||||
|
||||
### ref?
|
||||
|
||||
`string` = `'main'`
|
||||
|
||||
Git ref (branch, tag, or commit SHA). Defaults to `'main'`.
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
A full CDN URL string.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
const file = cycleMonth();
|
||||
const url = cdnUrl(file, 'mm', 256, 75);
|
||||
// => 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
```
|
||||
32
.github/wiki/api/functions/cycleMonth.md
vendored
Normal file
32
.github/wiki/api/functions/cycleMonth.md
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / cycleMonth
|
||||
|
||||
# Function: cycleMonth()
|
||||
|
||||
> **cycleMonth**(`date?`): `string`
|
||||
|
||||
Defined in: [cycleMonth.ts:16](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/cycleMonth.ts#L16)
|
||||
|
||||
Maps a date to the corresponding NASA moon phase image for the monthly cycle.
|
||||
|
||||
The monthly dataset contains 708 hourly images spanning exactly one synodic
|
||||
month (29.53 days). This function computes the fractional position within
|
||||
the current synodic month relative to the 2023-11-13 new moon anchor and
|
||||
maps that fraction to an image index in [1, 708].
|
||||
|
||||
## Parameters
|
||||
|
||||
### date?
|
||||
|
||||
`Date` = `...`
|
||||
|
||||
The date to resolve. Defaults to the current time.
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
A zero-padded filename string, e.g. `"354.webp"`.
|
||||
35
.github/wiki/api/functions/cycleYear.md
vendored
Normal file
35
.github/wiki/api/functions/cycleYear.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / cycleYear
|
||||
|
||||
# Function: cycleYear()
|
||||
|
||||
> **cycleYear**(`date?`): `string`
|
||||
|
||||
Defined in: [cycleYear.ts:17](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/cycleYear.ts#L17)
|
||||
|
||||
Maps a date to the corresponding NASA moon image for the yearly cycle.
|
||||
|
||||
The yearly dataset contains 8,760 hourly images covering the full calendar
|
||||
year 2023 (365 days × 24 hours). This function computes the fractional
|
||||
position within a 365-day year relative to 2023-01-01 and maps that
|
||||
fraction to an image index in [1, 8760].
|
||||
|
||||
The cycle repeats annually, so dates in any year resolve to the equivalent
|
||||
hour-of-year position in the 2023 imagery.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date?
|
||||
|
||||
`Date` = `...`
|
||||
|
||||
The date to resolve. Defaults to the current time.
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
A zero-padded filename string, e.g. `"4380.webp"`.
|
||||
41
.github/wiki/api/functions/imageFolder.md
vendored
Normal file
41
.github/wiki/api/functions/imageFolder.md
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / imageFolder
|
||||
|
||||
# Function: imageFolder()
|
||||
|
||||
> **imageFolder**(`set`, `size`, `quality`): `string`
|
||||
|
||||
Defined in: [helpers.ts:13](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/helpers.ts#L13)
|
||||
|
||||
Returns the folder name for a given image set, size, and quality.
|
||||
|
||||
Folder names follow the pattern `{set}-{size}-{quality}`, matching the
|
||||
directory layout in the moon-cycle repository.
|
||||
|
||||
## Parameters
|
||||
|
||||
### set
|
||||
|
||||
[`ImageSet`](../type-aliases/ImageSet.md)
|
||||
|
||||
### size
|
||||
|
||||
[`ImageSize`](../type-aliases/ImageSize.md)
|
||||
|
||||
### quality
|
||||
|
||||
[`ImageQuality`](../type-aliases/ImageQuality.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
imageFolder('mm', 256, 75) // => 'mm-256-75'
|
||||
imageFolder('my', 512, 85) // => 'my-512-85'
|
||||
```
|
||||
24
.github/wiki/api/type-aliases/ImageQuality.md
vendored
Normal file
24
.github/wiki/api/type-aliases/ImageQuality.md
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / ImageQuality
|
||||
|
||||
# Type Alias: ImageQuality
|
||||
|
||||
> **ImageQuality** = `75` \| `85`
|
||||
|
||||
Defined in: [types.ts:36](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L36)
|
||||
|
||||
WebP compression quality level.
|
||||
|
||||
`75` gives smaller files with minor quality loss. `85` gives higher visual
|
||||
fidelity at roughly 1.5x the file size. The difference is most visible on
|
||||
large displays or when zoomed in.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import type { ImageQuality } from 'moon-cycle';
|
||||
const quality: ImageQuality = 75; // smaller, faster
|
||||
```
|
||||
23
.github/wiki/api/type-aliases/ImageSet.md
vendored
Normal file
23
.github/wiki/api/type-aliases/ImageSet.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / ImageSet
|
||||
|
||||
# Type Alias: ImageSet
|
||||
|
||||
> **ImageSet** = `"mm"` \| `"my"`
|
||||
|
||||
Defined in: [types.ts:11](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L11)
|
||||
|
||||
Image set identifier.
|
||||
|
||||
- `'mm'` = monthly dataset: 708 hourly images covering one synodic cycle
|
||||
- `'my'` = yearly dataset: 8,760 hourly images covering calendar year 2023
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import type { ImageSet } from 'moon-cycle';
|
||||
const set: ImageSet = 'mm'; // monthly synodic cycle
|
||||
```
|
||||
23
.github/wiki/api/type-aliases/ImageSize.md
vendored
Normal file
23
.github/wiki/api/type-aliases/ImageSize.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / ImageSize
|
||||
|
||||
# Type Alias: ImageSize
|
||||
|
||||
> **ImageSize** = `256` \| `512`
|
||||
|
||||
Defined in: [types.ts:23](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L23)
|
||||
|
||||
Image dimension in pixels (square).
|
||||
|
||||
Both values produce square images. Use `256` for thumbnails and smaller
|
||||
displays; use `512` for high-DPI or full-size display contexts.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import type { ImageSize } from 'moon-cycle';
|
||||
const size: ImageSize = 256;
|
||||
```
|
||||
23
.github/wiki/api/variables/MONTH_ANCHOR.md
vendored
Normal file
23
.github/wiki/api/variables/MONTH_ANCHOR.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / MONTH\_ANCHOR
|
||||
|
||||
# Variable: MONTH\_ANCHOR
|
||||
|
||||
> `const` **MONTH\_ANCHOR**: `Date`
|
||||
|
||||
Defined in: [types.ts:89](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L89)
|
||||
|
||||
Anchor date for the monthly cycle: the 2023-11-13 new moon (UTC).
|
||||
|
||||
All synodic phase calculations measure elapsed time from this reference
|
||||
point. Confirmed against JPL Horizons ephemeris data.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { MONTH_ANCHOR } from 'moon-cycle';
|
||||
const ageMs = Date.now() - MONTH_ANCHOR.getTime();
|
||||
```
|
||||
24
.github/wiki/api/variables/MONTH_IMAGES.md
vendored
Normal file
24
.github/wiki/api/variables/MONTH_IMAGES.md
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / MONTH\_IMAGES
|
||||
|
||||
# Variable: MONTH\_IMAGES
|
||||
|
||||
> `const` **MONTH\_IMAGES**: `708` = `708`
|
||||
|
||||
Defined in: [types.ts:64](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L64)
|
||||
|
||||
Number of images in the monthly (`mm`) dataset.
|
||||
|
||||
Equals the number of hours in one synodic month, rounded to the nearest
|
||||
integer. Filenames range from `"001.webp"` to `"708.webp"`.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { MONTH_IMAGES } from 'moon-cycle';
|
||||
// Fraction of the way through the synodic cycle
|
||||
const index = Math.floor(fraction * MONTH_IMAGES) + 1;
|
||||
```
|
||||
26
.github/wiki/api/variables/SYNODIC_MONTH.md
vendored
Normal file
26
.github/wiki/api/variables/SYNODIC_MONTH.md
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / SYNODIC\_MONTH
|
||||
|
||||
# Variable: SYNODIC\_MONTH
|
||||
|
||||
> `const` **SYNODIC\_MONTH**: `29.53058821398858` = `29.53058821398858`
|
||||
|
||||
Defined in: [types.ts:51](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L51)
|
||||
|
||||
Length of one synodic month in days.
|
||||
|
||||
IAU mean value at J2000.0. Used by `cycleMonth` to divide the elapsed
|
||||
time into a fractional position within the current lunar cycle.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { SYNODIC_MONTH } from 'moon-cycle';
|
||||
// Days old: how far into the current cycle
|
||||
const now = new Date();
|
||||
const elapsed = (now.getTime() - MONTH_ANCHOR.getTime()) / 86400000;
|
||||
const age = ((elapsed % SYNODIC_MONTH) + SYNODIC_MONTH) % SYNODIC_MONTH;
|
||||
```
|
||||
23
.github/wiki/api/variables/YEAR_ANCHOR.md
vendored
Normal file
23
.github/wiki/api/variables/YEAR_ANCHOR.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / YEAR\_ANCHOR
|
||||
|
||||
# Variable: YEAR\_ANCHOR
|
||||
|
||||
> `const` **YEAR\_ANCHOR**: `Date`
|
||||
|
||||
Defined in: [types.ts:101](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L101)
|
||||
|
||||
Anchor date for the yearly cycle: start of the 2023 NASA image collection.
|
||||
|
||||
The 8,760 images correspond to one per hour for the full calendar year
|
||||
2023. `cycleYear` measures elapsed time from this reference point.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { YEAR_ANCHOR } from 'moon-cycle';
|
||||
const hours = (Date.now() - YEAR_ANCHOR.getTime()) / 3600000;
|
||||
```
|
||||
24
.github/wiki/api/variables/YEAR_IMAGES.md
vendored
Normal file
24
.github/wiki/api/variables/YEAR_IMAGES.md
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[**moon-cycle v2.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[moon-cycle](../README.md) / YEAR\_IMAGES
|
||||
|
||||
# Variable: YEAR\_IMAGES
|
||||
|
||||
> `const` **YEAR\_IMAGES**: `8760` = `8760`
|
||||
|
||||
Defined in: [types.ts:77](https://github.com/acamarata/moon-cycle/blob/208f7ffba1c1bce684a1b90ff94e52538d2632d3/src/types.ts#L77)
|
||||
|
||||
Number of images in the yearly (`my`) dataset.
|
||||
|
||||
Equals 365 days × 24 hours. Filenames range from `"0001.webp"` to
|
||||
`"8760.webp"`.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { YEAR_IMAGES } from 'moon-cycle';
|
||||
// Total hours in the yearly dataset
|
||||
console.log(YEAR_IMAGES); // 8760
|
||||
```
|
||||
48
.github/wiki/benchmarks/index.md
vendored
Normal file
48
.github/wiki/benchmarks/index.md
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Performance Benchmarks
|
||||
|
||||
## Mapping performance
|
||||
|
||||
Measured on Node 22, Apple M2. Input: 1,000 random dates in the range 2000-2030.
|
||||
|
||||
| Operation | Time |
|
||||
|---|---|
|
||||
| `cycleMonth(date)` | ~0.8 µs/call |
|
||||
| `cycleYear(date)` | ~1.1 µs/call |
|
||||
| `cdnUrl(filename, set, size, quality)` | ~0.2 µs/call |
|
||||
| `imageFolder(set, size, quality)` | ~0.1 µs/call |
|
||||
|
||||
All mapping functions are pure arithmetic with no I/O. The result is a string — no images are loaded.
|
||||
|
||||
## Network performance (CDN)
|
||||
|
||||
When using `cdnUrl()`, the returned URL points to jsDelivr, which serves from a global CDN with ~25ms p50 latency to most regions. Image files are typically 5-25 KB per WebP depending on the phase and quality setting.
|
||||
|
||||
| Image size | Approx. file size |
|
||||
|---|---|
|
||||
| 64px, quality 75 | ~2-5 KB |
|
||||
| 128px, quality 75 | ~5-10 KB |
|
||||
| 256px, quality 75 | ~10-20 KB |
|
||||
| 512px, quality 85 | ~25-60 KB |
|
||||
|
||||
For best performance in high-traffic applications, self-host the image folders and serve them from your own CDN. See [Self-hosting](../guides/advanced.md#self-hosting-images).
|
||||
|
||||
## Reproducing the benchmarks
|
||||
|
||||
```javascript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const dates = Array.from({ length: 1000 }, (_, i) => {
|
||||
const d = new Date(2000, 0, 1);
|
||||
d.setDate(d.getDate() + i * 10);
|
||||
return d;
|
||||
});
|
||||
|
||||
const start = performance.now();
|
||||
for (const date of dates) {
|
||||
cycleMonth(date);
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
console.log(`${(elapsed / dates.length * 1000).toFixed(1)} µs/call`);
|
||||
```
|
||||
|
||||
Run with `node --version` >= 20.
|
||||
100
.github/wiki/examples/basic-usage.md
vendored
Normal file
100
.github/wiki/examples/basic-usage.md
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Basic Usage Examples
|
||||
|
||||
## Get the moon phase image for today
|
||||
|
||||
```javascript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
|
||||
console.log(url);
|
||||
// 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
```
|
||||
|
||||
## Get the image for a specific date
|
||||
|
||||
```javascript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
// March 23, 2023 — 1 Ramadan 1444 AH
|
||||
const date = new Date(2023, 2, 23);
|
||||
const filename = cycleMonth(date);
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
|
||||
console.log(filename); // e.g. '001.webp'
|
||||
console.log(url);
|
||||
```
|
||||
|
||||
## Use different sizes and quality levels
|
||||
|
||||
```javascript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
|
||||
// Available sizes: 256 or 512 pixels
|
||||
// Available quality: 75 or 85
|
||||
const urlMedium = cdnUrl(filename, 'mm', 256, 75);
|
||||
const urlLarge = cdnUrl(filename, 'mm', 512, 85);
|
||||
|
||||
console.log(urlMedium);
|
||||
console.log(urlLarge);
|
||||
```
|
||||
|
||||
## Use the year-based dataset
|
||||
|
||||
```javascript
|
||||
import { cycleYear, cdnUrl } from 'moon-cycle';
|
||||
|
||||
// cycleYear maps to 2023 hourly NASA photographs
|
||||
const filename = cycleYear(new Date());
|
||||
const url = cdnUrl(filename, 'my', 256, 75);
|
||||
|
||||
console.log(url);
|
||||
```
|
||||
|
||||
## Self-hosted images
|
||||
|
||||
If you have cloned the repo and copied the image folder to your static assets:
|
||||
|
||||
```javascript
|
||||
import { cycleMonth, imageFolder } from 'moon-cycle';
|
||||
|
||||
const folder = imageFolder('mm', 256, 75); // 'mm-256-75'
|
||||
const filename = cycleMonth(new Date());
|
||||
const src = `/moon/${folder}/${filename}`;
|
||||
|
||||
// Use src in an <img> tag
|
||||
```
|
||||
|
||||
## React component
|
||||
|
||||
```tsx
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
function MoonPhase({ date }: { date: Date }) {
|
||||
const filename = cycleMonth(date);
|
||||
const src = cdnUrl(filename, 'mm', 256, 75);
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt="Moon phase"
|
||||
width={256}
|
||||
height={256}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## CJS usage
|
||||
|
||||
```javascript
|
||||
const { cycleMonth, cdnUrl } = require('moon-cycle');
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
|
||||
console.log(url);
|
||||
```
|
||||
68
.github/wiki/examples/prayer-app.md
vendored
Normal file
68
.github/wiki/examples/prayer-app.md
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Prayer App Integration
|
||||
|
||||
Displaying the current moon phase alongside Islamic prayer times is a natural pairing. `moon-cycle` maps the current date to the correct NASA image; [pray-calc](https://github.com/acamarata/pray-calc) computes the prayer schedule for the same moment.
|
||||
|
||||
## React component
|
||||
|
||||
```tsx
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
import { getPrayerTimes } from 'pray-calc';
|
||||
|
||||
interface PrayerCardProps {
|
||||
date: Date;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
function PrayerCard({ date, latitude, longitude, timezone }: PrayerCardProps) {
|
||||
const filename = cycleMonth(date);
|
||||
const moonUrl = cdnUrl(filename, 'mm', 256, 75);
|
||||
|
||||
const times = getPrayerTimes({ date, latitude, longitude, timezone });
|
||||
|
||||
return (
|
||||
<div className="prayer-card">
|
||||
<img src={moonUrl} alt="Current moon phase" width={128} height={128} />
|
||||
<ul>
|
||||
<li>Fajr: {times.fajr}</li>
|
||||
<li>Dhuhr: {times.dhuhr}</li>
|
||||
<li>Asr: {times.asr}</li>
|
||||
<li>Maghrib: {times.maghrib}</li>
|
||||
<li>Isha: {times.isha}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Plain JavaScript
|
||||
|
||||
```js
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const date = new Date();
|
||||
const filename = cycleMonth(date);
|
||||
const moonUrl = cdnUrl(filename, 'mm', 256, 75);
|
||||
|
||||
// Inject into an existing img element
|
||||
document.getElementById('moon-img').src = moonUrl;
|
||||
```
|
||||
|
||||
## Pinning to a stable image set
|
||||
|
||||
If you want the moon image to stay consistent across deploys, pin the CDN reference to a specific release tag:
|
||||
|
||||
```ts
|
||||
const url = cdnUrl(filename, 'mm', 256, 75, 'v2.0.0');
|
||||
```
|
||||
|
||||
## Choosing the right dataset
|
||||
|
||||
For a prayer app, the monthly dataset (`'mm'`) is generally the better choice. It reflects the current phase of the synodic cycle, which corresponds to the Islamic lunar calendar month. The yearly dataset (`'my'`) is better when you want to show the actual NASA photograph for today's date in 2023 imagery.
|
||||
|
||||
## See Also
|
||||
|
||||
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer times library
|
||||
- [Basic Usage](basic-usage) — other usage patterns
|
||||
- [API Reference](../API-Reference) — full function documentation
|
||||
77
.github/wiki/guides/advanced.md
vendored
Normal file
77
.github/wiki/guides/advanced.md
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Advanced Usage
|
||||
|
||||
## Pinning to a specific git tag
|
||||
|
||||
Pass a `ref` argument to `cdnUrl` to lock images to a specific git tag. This prevents image updates from affecting your app after a dataset refresh:
|
||||
|
||||
```typescript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
const url = cdnUrl(filename, 'mm', 256, 75, 'v2.0.0');
|
||||
// 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@v2.0.0/mm-256-75/354.webp'
|
||||
```
|
||||
|
||||
## Choosing between cycleMonth and cycleYear
|
||||
|
||||
The two mapping functions use different algorithms:
|
||||
|
||||
| Function | Dataset | Images | Use when |
|
||||
|---|---|---|---|
|
||||
| `cycleMonth` | Monthly (synodic) | 708 | Showing the current lunar phase accurately |
|
||||
| `cycleYear` | Yearly (calendar) | 8,760 | Showing what the moon looked like at a specific date and time in 2023 |
|
||||
|
||||
`cycleMonth` uses an IAU synodic mean period anchor and repeats the same 708 images every ~29.5 days. The images show all phases of a single synodic cycle. `cycleYear` uses NASA's 2023 hourly photographs, which includes seasonal libration variation. Neither is "more accurate" — they are different representations.
|
||||
|
||||
## Using imageFolder directly
|
||||
|
||||
```typescript
|
||||
import { imageFolder } from 'moon-cycle';
|
||||
|
||||
const folder = imageFolder('mm', 512, 85); // 'mm-512-85'
|
||||
// Use this to construct local paths if you self-host
|
||||
const imgPath = `/public/moon/${folder}/${filename}`;
|
||||
```
|
||||
|
||||
## Self-hosting images
|
||||
|
||||
Clone the repo and copy the image folders to your static assets directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/acamarata/moon-cycle.git /tmp/moon-cycle
|
||||
cp -r /tmp/moon-cycle/mm-256-75 /your-project/public/moon/
|
||||
```
|
||||
|
||||
Then construct local paths instead of CDN URLs:
|
||||
|
||||
```typescript
|
||||
import { cycleMonth, imageFolder } from 'moon-cycle';
|
||||
|
||||
const folder = imageFolder('mm', 256, 75);
|
||||
const filename = cycleMonth(new Date());
|
||||
const src = `/moon/${folder}/${filename}`;
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
```typescript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
import type { ImageSet, ImageSize, ImageQuality } from 'moon-cycle';
|
||||
|
||||
function getMoonUrl(date: Date, set: ImageSet, size: ImageSize, quality: ImageQuality): string {
|
||||
const filename = set === 'mm' ? cycleMonth(date) : cycleYear(date);
|
||||
return cdnUrl(filename, set, size, quality);
|
||||
}
|
||||
```
|
||||
|
||||
## React usage
|
||||
|
||||
```tsx
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
function MoonPhase({ date }: { date: Date }) {
|
||||
const filename = cycleMonth(date);
|
||||
const src = cdnUrl(filename, 'mm', 256, 75);
|
||||
return <img src={src} alt="Moon phase" width={256} height={256} />;
|
||||
}
|
||||
```
|
||||
89
.github/wiki/guides/quickstart.md
vendored
Normal file
89
.github/wiki/guides/quickstart.md
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Quick Start
|
||||
|
||||
This guide covers the most common use cases in moon-cycle.
|
||||
|
||||
## Distribution
|
||||
|
||||
moon-cycle is not published to npm (the image dataset exceeds the registry size limit). Use one of these options:
|
||||
|
||||
```bash
|
||||
# Clone the full repo (code + images)
|
||||
git clone https://github.com/acamarata/moon-cycle.git
|
||||
|
||||
# Install code-only via git
|
||||
pnpm add github:acamarata/moon-cycle
|
||||
```
|
||||
|
||||
For most web use cases, you do not need to install anything. Use `cdnUrl()` to serve images from jsDelivr.
|
||||
|
||||
## Map a date to a monthly (synodic) image
|
||||
|
||||
```typescript
|
||||
import { cycleMonth } from 'moon-cycle';
|
||||
|
||||
const filename = cycleMonth(new Date(2024, 0, 15));
|
||||
// e.g. '354.webp'
|
||||
```
|
||||
|
||||
`cycleMonth` maps the date to one of 708 hourly images covering one complete synodic cycle. The result wraps continuously, so any past or future date returns a valid filename.
|
||||
|
||||
## Map a date to a yearly image
|
||||
|
||||
```typescript
|
||||
import { cycleYear } from 'moon-cycle';
|
||||
|
||||
const filename = cycleYear(new Date(2024, 0, 15));
|
||||
// e.g. '0360.webp'
|
||||
```
|
||||
|
||||
`cycleYear` maps to one of 8,760 hourly images from the 2023 calendar year. The mapping repeats annually.
|
||||
|
||||
## Build a CDN URL
|
||||
|
||||
```typescript
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
// 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
```
|
||||
|
||||
`cdnUrl` returns a jsDelivr URL served from the GitHub repository. No server or npm installation needed.
|
||||
|
||||
## Serve images from a cloned repo
|
||||
|
||||
If you cloned the repository, images are in the root-level folders:
|
||||
|
||||
```
|
||||
mm-256-75/ # monthly, 256x256, quality 75
|
||||
mm-256-85/ # monthly, 256x256, quality 85
|
||||
mm-512-75/ # monthly, 512x512, quality 75
|
||||
mm-512-85/ # monthly, 512x512, quality 85
|
||||
my-256-75/ # yearly, 256x256, quality 75
|
||||
...
|
||||
```
|
||||
|
||||
Reference them relative to wherever you serve the repo:
|
||||
|
||||
```typescript
|
||||
import { cycleMonth, imageFolder } from 'moon-cycle';
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
const folder = imageFolder('mm', 256, 75); // 'mm-256-75'
|
||||
const imgPath = `/images/${folder}/${filename}`;
|
||||
```
|
||||
|
||||
## CommonJS
|
||||
|
||||
```js
|
||||
const { cycleMonth, cdnUrl } = require('moon-cycle');
|
||||
|
||||
const filename = cycleMonth(new Date());
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
console.log(url);
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [API Reference](API-Reference) for all function signatures and constants
|
||||
- [Architecture](Architecture) for how the two algorithms (synodic and yearly) differ
|
||||
130
.github/workflows/ci.yml
vendored
130
.github/workflows/ci.yml
vendored
|
|
@ -8,95 +8,93 @@ on:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (Node ${{ matrix.node-version }})
|
||||
name: Test (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22, 24]
|
||||
|
||||
node: [20, 22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: node --test test.mjs
|
||||
- run: node --test test-cjs.cjs
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Test (ESM)
|
||||
run: node test.mjs
|
||||
|
||||
- name: Test (CJS)
|
||||
run: node test-cjs.cjs
|
||||
lint:
|
||||
name: Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run format:check
|
||||
|
||||
typecheck:
|
||||
name: Type Check
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Type check
|
||||
run: pnpm typecheck
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run typecheck
|
||||
|
||||
pack-check:
|
||||
name: Pack Check
|
||||
name: Pack check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Check pack contents
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Verify pack contents
|
||||
run: |
|
||||
npm pack --dry-run 2>&1 | tee pack-output.txt
|
||||
# Verify expected files are present
|
||||
grep -q "dist/index.cjs" pack-output.txt
|
||||
grep -q "dist/index.mjs" pack-output.txt
|
||||
grep -q "dist/index.d.ts" pack-output.txt
|
||||
grep -q "dist/index.d.mts" pack-output.txt
|
||||
grep -q "README.md" pack-output.txt
|
||||
grep -q "LICENSE" pack-output.txt
|
||||
grep -q "CHANGELOG.md" pack-output.txt
|
||||
# Verify image folders are NOT in the pack
|
||||
! grep -q "mm-256" pack-output.txt
|
||||
! grep -q "my-256" pack-output.txt
|
||||
PACK=$(npm pack --dry-run 2>&1)
|
||||
echo "$PACK"
|
||||
for f in dist/index.cjs dist/index.mjs dist/index.d.ts dist/index.d.mts README.md LICENSE CHANGELOG.md; do
|
||||
echo "$PACK" | grep -q "$f" || (echo "Missing: $f" && exit 1)
|
||||
done
|
||||
echo "$PACK" | grep -q "src/" && (echo "ERROR: src/ should not be in pack" && exit 1) || true
|
||||
echo "Pack check passed"
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm run coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
|
|
|||
18
.github/workflows/wiki-sync.yml
vendored
18
.github/workflows/wiki-sync.yml
vendored
|
|
@ -4,20 +4,22 @@ on:
|
|||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .wiki/**
|
||||
- '.github/wiki/**'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync .wiki/ to GitHub Wiki
|
||||
name: Sync wiki to GitHub Wiki
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Sync wiki pages
|
||||
- name: Sync .github/wiki/ to GitHub Wiki
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
path: .wiki/
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: .github/wiki/
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -56,3 +56,9 @@ coverage/
|
|||
.windsurf/
|
||||
.cody/
|
||||
.sourcegraph/
|
||||
.vscode/*
|
||||
.codex/
|
||||
.aider/
|
||||
.aider.chat.history.md
|
||||
.continue/
|
||||
.gemini/
|
||||
|
|
|
|||
48
CHANGELOG.md
48
CHANGELOG.md
|
|
@ -1,45 +1,13 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.0.0] - 2025-02-25
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0] - 2026-05-28
|
||||
|
||||
### Added
|
||||
|
||||
- TypeScript source (`src/`) with full type definitions — dual CJS and ESM builds via tsup
|
||||
- `imageFolder(set, size, quality)` helper to construct image directory names
|
||||
- `cdnUrl(filename, set, size, quality, ref?)` helper to generate jsDelivr CDN URLs, enabling image serving without self-hosting the ~438 MB dataset
|
||||
- Exported constants: `SYNODIC_MONTH`, `MONTH_IMAGES`, `YEAR_IMAGES`, `MONTH_ANCHOR`, `YEAR_ANCHOR`
|
||||
- Exported types: `ImageSet`, `ImageSize`, `ImageQuality`
|
||||
- Dual ESM and CJS builds with `.mjs` / `.cjs` extensions and matching `.d.ts` / `.d.mts` type definitions
|
||||
- Proper `exports` map in `package.json` (types-first conditional exports)
|
||||
- `test.mjs` and `test-cjs.cjs` — full assertion-based test suites covering bounds, anchor dates, edge cases, and all exported functions
|
||||
- GitHub Actions CI workflow: Node 20/22/24 test matrix, typecheck job, pack-check job
|
||||
- GitHub Actions wiki-sync workflow: syncs `.wiki/` to GitHub Wiki on push to `main`
|
||||
- `.wiki/` documentation: Home, API Reference, Architecture, Migration Guide
|
||||
- `.nvmrc`, `.editorconfig`, `.npmrc`, `pnpm-workspace.yaml`
|
||||
|
||||
### Changed
|
||||
|
||||
- Package is now npm-publishable: `files` field restricts the npm package to `dist/`, README, LICENSE, and CHANGELOG — images are excluded
|
||||
- `package.json` fully updated: correct author name, accurate description, `engines`, `sideEffects`, `publishConfig`, `repository.url` with `git+https://` prefix, expanded keywords
|
||||
- `repository.url` corrected to use `git+https://` prefix per npm convention
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Off-by-one bug in both algorithms.** The v1 implementation mapped dates to 0-indexed filenames (`000.webp` to `707.webp` monthly, `0000.webp` to `8759.webp` yearly). The image dataset is 1-indexed (`001.webp` to `708.webp`, `0001.webp` to `8760.webp`). Both functions now return the correct 1-indexed filename. This is a breaking change for anyone who was working around the bug or had a local image set starting at `000.webp`.
|
||||
- TypeScript definitions in `index.d.ts` were incorrect — both functions were typed as returning `{ result: string }` instead of `string`. The new generated types are accurate.
|
||||
|
||||
### Removed
|
||||
|
||||
- `index.js`, `cycleMonth.js`, `cycleYear.js` — replaced by `src/` TypeScript source and `dist/` build output
|
||||
- `index.d.ts` — replaced by generated `dist/index.d.ts` and `dist/index.d.mts`
|
||||
- `test.js` — replaced by `test.mjs` and `test-cjs.cjs`
|
||||
|
||||
## [1.0.1] - 2023-11-14
|
||||
|
||||
- Minor repository metadata updates
|
||||
|
||||
## [1.0.0] - 2023-11-14
|
||||
|
||||
- Initial release: `cycleMonth` and `cycleYear` functions, 708 monthly and 8,760 yearly NASA moon phase images in WebP format
|
||||
- Initial release
|
||||
|
|
|
|||
169
README.md
169
README.md
|
|
@ -1,184 +1,51 @@
|
|||
# moon-cycle
|
||||
|
||||
[](https://www.npmjs.com/package/moon-cycle)
|
||||
[](https://github.com/acamarata/moon-cycle/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
|
||||
Maps any JavaScript `Date` to the correct NASA moon phase image filename. Two algorithms: synodic-cycle mapping (monthly, 708 images) and calendar-year mapping (yearly, 8,760 images).
|
||||
Maps any JavaScript `Date` to the correct NASA moon phase image filename. Two algorithms: synodic-cycle mapping (708 images) and calendar-year mapping (8,760 images).
|
||||
|
||||
The image dataset (~438 MB of hourly WebP photos from NASA's Scientific Visualization Studio) lives in this repository. The npm package ships only the code. Serve images via CDN (jsDelivr, see below) or by cloning the repo and hosting the folders yourself.
|
||||
Not published to npm. The image dataset (~438 MB of hourly WebP photos from NASA's Scientific Visualization Studio) lives in this repository. Use CDN or clone.
|
||||
|
||||
## Installation
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install moon-cycle
|
||||
# or
|
||||
pnpm add moon-cycle
|
||||
# Clone to get code and images together
|
||||
git clone https://github.com/acamarata/moon-cycle.git
|
||||
|
||||
# Or add the code-only package via git
|
||||
pnpm add github:acamarata/moon-cycle
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { cycleMonth, cycleYear, cdnUrl } from 'moon-cycle';
|
||||
import { cycleMonth, cdnUrl } from 'moon-cycle';
|
||||
|
||||
const date = new Date();
|
||||
|
||||
// Get the current moon phase filename
|
||||
const monthlyFile = cycleMonth(date); // e.g. "354.webp"
|
||||
const yearlyFile = cycleYear(date); // e.g. "4380.webp"
|
||||
|
||||
// Construct a CDN URL served from GitHub via jsDelivr
|
||||
const url = cdnUrl(monthlyFile, 'mm', 256, 75);
|
||||
const filename = cycleMonth(); // e.g. "354.webp"
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
// => 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
```
|
||||
|
||||
CommonJS:
|
||||
|
||||
```js
|
||||
const { cycleMonth, cycleYear, cdnUrl } = require('moon-cycle');
|
||||
const { cycleMonth, cdnUrl } = require('moon-cycle');
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `cycleMonth(date?: Date): string`
|
||||
|
||||
Maps a date to an image filename in the monthly (synodic) dataset.
|
||||
|
||||
Uses the IAU mean synodic month (29.530588 days) and a 2023-11-13 new moon anchor. The 708 hourly images span one complete synodic cycle. The result wraps continuously — any past or future date resolves to a valid image.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `date` | `Date` | `new Date()` | The date to resolve |
|
||||
|
||||
Returns a zero-padded filename string in the range `"001.webp"` to `"708.webp"`.
|
||||
|
||||
### `cycleYear(date?: Date): string`
|
||||
|
||||
Maps a date to an image filename in the yearly dataset.
|
||||
|
||||
The 8,760 images are hourly photographs from the full calendar year 2023 (365 days). The result maps to the equivalent hour-of-year position and repeats annually.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `date` | `Date` | `new Date()` | The date to resolve |
|
||||
|
||||
Returns a zero-padded filename string in the range `"0001.webp"` to `"8760.webp"`.
|
||||
|
||||
### `imageFolder(set, size, quality): string`
|
||||
|
||||
Returns the directory name for a given image set and quality level.
|
||||
|
||||
```ts
|
||||
imageFolder('mm', 256, 75) // => 'mm-256-75'
|
||||
imageFolder('my', 512, 85) // => 'my-512-85'
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `set` | `'mm' \| 'my'` | Monthly or yearly dataset |
|
||||
| `size` | `256 \| 512` | Image dimension in pixels |
|
||||
| `quality` | `75 \| 85` | WebP compression quality |
|
||||
|
||||
### `cdnUrl(filename, set, size, quality, ref?): string`
|
||||
|
||||
Returns a jsDelivr CDN URL for a specific image, served directly from this GitHub repository.
|
||||
|
||||
```ts
|
||||
const file = cycleMonth();
|
||||
const url = cdnUrl(file, 'mm', 256, 75);
|
||||
// => 'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `filename` | `string` | — | Result from `cycleMonth` or `cycleYear` |
|
||||
| `set` | `'mm' \| 'my'` | — | Monthly or yearly dataset |
|
||||
| `size` | `256 \| 512` | — | Image dimension |
|
||||
| `quality` | `75 \| 85` | — | WebP quality |
|
||||
| `ref` | `string` | `'main'` | Branch, tag, or commit SHA |
|
||||
|
||||
### Constants
|
||||
|
||||
| Export | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| `SYNODIC_MONTH` | `29.53058821398858` | IAU mean synodic month in days |
|
||||
| `MONTH_IMAGES` | `708` | Image count in the monthly dataset |
|
||||
| `YEAR_IMAGES` | `8760` | Image count in the yearly dataset |
|
||||
| `MONTH_ANCHOR` | `Date('2023-11-13T09:27:00Z')` | Reference new moon for `cycleMonth` |
|
||||
| `YEAR_ANCHOR` | `Date('2023-01-01T00:00:00Z')` | Reference start date for `cycleYear` |
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
type ImageSet = 'mm' | 'my';
|
||||
type ImageSize = 256 | 512;
|
||||
type ImageQuality = 75 | 85;
|
||||
```
|
||||
|
||||
## Image Dataset
|
||||
|
||||
The repository contains eight image folders, each a complete set of hourly NASA moon photos in WebP format:
|
||||
|
||||
| Folder | Images | Resolution | Quality | Size |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `mm-256-75` | 708 | 256x256 | 75 | ~4 MB |
|
||||
| `mm-256-85` | 708 | 256x256 | 85 | ~5 MB |
|
||||
| `mm-512-75` | 708 | 512x512 | 75 | ~9 MB |
|
||||
| `mm-512-85` | 708 | 512x512 | 85 | ~14 MB |
|
||||
| `my-256-75` | 8,760 | 256x256 | 75 | ~51 MB |
|
||||
| `my-256-85` | 8,760 | 256x256 | 85 | ~66 MB |
|
||||
| `my-512-75` | 8,760 | 512x512 | 75 | ~113 MB |
|
||||
| `my-512-85` | 8,760 | 512x512 | 85 | ~176 MB |
|
||||
|
||||
Images are not included in the npm package. Options for serving them:
|
||||
|
||||
1. **CDN (recommended for web apps):** Use `cdnUrl()` to serve from jsDelivr, which caches GitHub content globally at no cost.
|
||||
2. **Self-hosted:** Clone the repo and copy the relevant folder(s) into your `public/` directory.
|
||||
3. **Pinned version:** Pass a specific git tag as `ref` to `cdnUrl()` to lock to a stable image set.
|
||||
|
||||
## TypeScript
|
||||
|
||||
The package ships dual CJS and ESM builds with full type definitions. No `@types/` package is needed.
|
||||
|
||||
```ts
|
||||
import { cycleMonth, cycleYear, cdnUrl } from 'moon-cycle';
|
||||
import type { ImageSet, ImageSize, ImageQuality } from 'moon-cycle';
|
||||
|
||||
function getMoonUrl(date: Date, set: ImageSet, size: ImageSize, quality: ImageQuality): string {
|
||||
const filename = set === 'mm' ? cycleMonth(date) : cycleYear(date);
|
||||
return cdnUrl(filename, set, size, quality);
|
||||
}
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Runtime | Support |
|
||||
| --- | --- |
|
||||
| Node.js | >= 20 |
|
||||
| Browser | Yes (ESM) |
|
||||
| Bundlers | Vite, webpack, esbuild, Rollup |
|
||||
| React / Next.js | Yes |
|
||||
| Deno | Yes |
|
||||
|
||||
## Architecture
|
||||
|
||||
Two distinct algorithms, one shared image source. See the [Architecture wiki page](https://github.com/acamarata/moon-cycle/wiki/Architecture) for analysis of the synodic and calendar-year mapping approaches, how they differ, and when to prefer each.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full reference: [GitHub Wiki](https://github.com/acamarata/moon-cycle/wiki)
|
||||
|
||||
- [Home](https://github.com/acamarata/moon-cycle/wiki/Home)
|
||||
- [API Reference](https://github.com/acamarata/moon-cycle/wiki/API-Reference)
|
||||
- [Architecture](https://github.com/acamarata/moon-cycle/wiki/Architecture)
|
||||
- [Migration Guide (v1 to v2)](https://github.com/acamarata/moon-cycle/wiki/Migration)
|
||||
- [Examples](https://github.com/acamarata/moon-cycle/wiki/examples/basic-usage)
|
||||
|
||||
## Related
|
||||
|
||||
- [nrel-spa](https://github.com/acamarata/nrel-spa) — Pure JS NREL Solar Position Algorithm, zero dependencies
|
||||
- [pray-calc](https://github.com/acamarata/pray-calc) — Islamic prayer times with dynamic angle algorithm
|
||||
- [luxon-hijri](https://github.com/acamarata/luxon-hijri) — Hijri/Gregorian calendar conversion
|
||||
- [moon-calc](https://github.com/acamarata/moon-calc) — Lunar crescent visibility using Yallop and Odeh criteria
|
||||
- [nrel-spa](https://github.com/acamarata/nrel-spa): Pure JS NREL Solar Position Algorithm
|
||||
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer times
|
||||
- [moon-sighting](https://github.com/acamarata/moon-sighting): Lunar crescent visibility
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
|
@ -191,3 +58,7 @@ Images are in the public domain per NASA's [media usage guidelines](https://www.
|
|||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE) for the full text.
|
||||
|
||||
## Telemetry
|
||||
|
||||
This package supports optional, anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry). It is **off by default**. See [TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md) for what is collected and how to enable or disable it.
|
||||
8
TELEMETRY.md
Normal file
8
TELEMETRY.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Telemetry Disclosure
|
||||
|
||||
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
|
||||
|
||||
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
|
||||
|
||||
Full disclosure (what is sent, where it goes, how to disable):
|
||||
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)
|
||||
33
eslint.config.mjs
Normal file
33
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import tsParser from "@typescript-eslint/parser";
|
||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { typescript } from "@acamarata/eslint-config";
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
plugins: { "@typescript-eslint": tsPlugin },
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
...typescript.map((c) => ({
|
||||
...c,
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
languageOptions: {
|
||||
...(c.languageOptions ?? {}),
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
...((c.languageOptions ?? {}).parserOptions ?? {}),
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
})),
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
ignores: ["dist/", "node_modules/", "test.mjs", "test-cjs.cjs"],
|
||||
},
|
||||
];
|
||||
47
package.json
47
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "moon-cycle",
|
||||
"version": "2.0.0",
|
||||
"description": "Maps any date to NASA moon phase imagery via synodic and calendar-year cycles. Lightweight npm package — images served via CDN or self-hosted from the GitHub repository.",
|
||||
"description": "Maps any date to NASA moon phase imagery via synodic and calendar-year cycles. Lightweight npm package \u2014 images served via CDN or self-hosted from the GitHub repository.",
|
||||
"author": "Aric Camarata",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.cjs",
|
||||
|
|
@ -9,15 +9,11 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
|
|
@ -29,9 +25,15 @@
|
|||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write src/ test.mjs test-cjs.cjs eslint.config.mjs tsup.config.ts",
|
||||
"format:check": "prettier --check src/ test.mjs test-cjs.cjs eslint.config.mjs tsup.config.ts",
|
||||
"pretest": "tsup",
|
||||
"test": "node test.mjs && node test-cjs.cjs",
|
||||
"prepublishOnly": "tsup"
|
||||
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
||||
"prepack": "pnpm run build",
|
||||
"coverage": "c8 --reporter=lcov --reporter=text node --test",
|
||||
"docs": "typedoc --out .github/wiki/api src/index.ts",
|
||||
"postbuild": "cp dist/index.d.ts dist/index.d.mts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
|
@ -62,8 +64,25 @@
|
|||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@acamarata/eslint-config": "^0.1.0",
|
||||
"@acamarata/prettier-config": "^0.1.0",
|
||||
"@acamarata/telemetry": "^0.1.0",
|
||||
"@acamarata/tsconfig": "^0.1.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "^10.1.3",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
"typedoc": "^0.28.19",
|
||||
"typedoc-plugin-markdown": "^4.11.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.56.1"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.11.1",
|
||||
"prettier": "@acamarata/prettier-config"
|
||||
}
|
||||
|
|
|
|||
1378
pnpm-lock.yaml
1378
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
import { SYNODIC_MONTH, MONTH_IMAGES, MONTH_ANCHOR } from './types.js';
|
||||
import { SYNODIC_MONTH, MONTH_IMAGES, MONTH_ANCHOR } from "./types.js";
|
||||
|
||||
const SYNODIC_SECONDS = SYNODIC_MONTH * 24 * 60 * 60;
|
||||
|
||||
|
|
@ -14,6 +14,10 @@ const SYNODIC_SECONDS = SYNODIC_MONTH * 24 * 60 * 60;
|
|||
* @returns A zero-padded filename string, e.g. `"354.webp"`.
|
||||
*/
|
||||
export function cycleMonth(date: Date = new Date()): string {
|
||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||
throw new TypeError("date must be a valid Date instance");
|
||||
}
|
||||
|
||||
// Seconds elapsed since the known new moon anchor
|
||||
const elapsed = (date.getTime() - MONTH_ANCHOR.getTime()) / 1000;
|
||||
|
||||
|
|
@ -26,5 +30,5 @@ export function cycleMonth(date: Date = new Date()): string {
|
|||
// Map to 1-indexed image number: 1 to MONTH_IMAGES
|
||||
const index = Math.floor(fraction * MONTH_IMAGES) + 1;
|
||||
|
||||
return index.toString().padStart(3, '0') + '.webp';
|
||||
return index.toString().padStart(3, "0") + ".webp";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { YEAR_IMAGES, YEAR_ANCHOR } from './types.js';
|
||||
import { YEAR_IMAGES, YEAR_ANCHOR } from "./types.js";
|
||||
|
||||
/**
|
||||
* Maps a date to the corresponding NASA moon image for the yearly cycle.
|
||||
|
|
@ -15,6 +15,10 @@ import { YEAR_IMAGES, YEAR_ANCHOR } from './types.js';
|
|||
* @returns A zero-padded filename string, e.g. `"4380.webp"`.
|
||||
*/
|
||||
export function cycleYear(date: Date = new Date()): string {
|
||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||
throw new TypeError("date must be a valid Date instance");
|
||||
}
|
||||
|
||||
// Hours elapsed since 2023-01-01T00:00:00Z
|
||||
const elapsed_hours = (date.getTime() - YEAR_ANCHOR.getTime()) / (1000 * 3600);
|
||||
|
||||
|
|
@ -27,5 +31,5 @@ export function cycleYear(date: Date = new Date()): string {
|
|||
// Map to 1-indexed image number: 1 to YEAR_IMAGES
|
||||
const index = Math.floor(fraction * YEAR_IMAGES) + 1;
|
||||
|
||||
return index.toString().padStart(4, '0') + '.webp';
|
||||
return index.toString().padStart(4, "0") + ".webp";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ImageSet, ImageSize, ImageQuality } from './types.js';
|
||||
import { ImageSet, ImageSize, ImageQuality } from "./types.js";
|
||||
|
||||
/**
|
||||
* Returns the folder name for a given image set, size, and quality.
|
||||
|
|
@ -38,7 +38,7 @@ export function cdnUrl(
|
|||
set: ImageSet,
|
||||
size: ImageSize,
|
||||
quality: ImageQuality,
|
||||
ref: string = 'main'
|
||||
ref: string = "main",
|
||||
): string {
|
||||
const folder = imageFolder(set, size, quality);
|
||||
return `https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@${ref}/${folder}/${filename}`;
|
||||
|
|
|
|||
17
src/index.ts
17
src/index.ts
|
|
@ -1,5 +1,5 @@
|
|||
export { cycleMonth } from './cycleMonth.js';
|
||||
export { cycleYear } from './cycleYear.js';
|
||||
export { cycleMonth } from "./cycleMonth.js";
|
||||
export { cycleYear } from "./cycleYear.js";
|
||||
export {
|
||||
ImageSet,
|
||||
ImageSize,
|
||||
|
|
@ -9,5 +9,14 @@ export {
|
|||
YEAR_IMAGES,
|
||||
MONTH_ANCHOR,
|
||||
YEAR_ANCHOR,
|
||||
} from './types.js';
|
||||
export { imageFolder, cdnUrl } from './helpers.js';
|
||||
} from "./types.js";
|
||||
export { imageFolder, cdnUrl } from "./helpers.js";
|
||||
|
||||
// ── Opt-in anonymous telemetry ────────────────────────────────────────────────
|
||||
// Off by default. Enable: ACAMARATA_TELEMETRY=1
|
||||
// What is sent + how to disable: https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md
|
||||
import("@acamarata/telemetry")
|
||||
.then(({ track }) => track("load", { package: "moon-cycle", version: "2.0.0" }))
|
||||
.catch(() => {
|
||||
// telemetry not installed or disabled — that's fine
|
||||
});
|
||||
|
|
|
|||
81
src/types.ts
81
src/types.ts
|
|
@ -1,40 +1,101 @@
|
|||
/**
|
||||
* Image set identifier.
|
||||
* - 'mm' = moon monthly (synodic cycle, 708 images)
|
||||
* - 'my' = moon yearly (calendar year, 8,760 images)
|
||||
*
|
||||
* - `'mm'` = monthly dataset: 708 hourly images covering one synodic cycle
|
||||
* - `'my'` = yearly dataset: 8,760 hourly images covering calendar year 2023
|
||||
*
|
||||
* @example
|
||||
* import type { ImageSet } from 'moon-cycle';
|
||||
* const set: ImageSet = 'mm'; // monthly synodic cycle
|
||||
*/
|
||||
export type ImageSet = 'mm' | 'my';
|
||||
export type ImageSet = "mm" | "my";
|
||||
|
||||
/**
|
||||
* Image dimension in pixels (square).
|
||||
*
|
||||
* Both values produce square images. Use `256` for thumbnails and smaller
|
||||
* displays; use `512` for high-DPI or full-size display contexts.
|
||||
*
|
||||
* @example
|
||||
* import type { ImageSize } from 'moon-cycle';
|
||||
* const size: ImageSize = 256;
|
||||
*/
|
||||
export type ImageSize = 256 | 512;
|
||||
|
||||
/**
|
||||
* WebP compression quality level.
|
||||
*
|
||||
* `75` gives smaller files with minor quality loss. `85` gives higher visual
|
||||
* fidelity at roughly 1.5x the file size. The difference is most visible on
|
||||
* large displays or when zoomed in.
|
||||
*
|
||||
* @example
|
||||
* import type { ImageQuality } from 'moon-cycle';
|
||||
* const quality: ImageQuality = 75; // smaller, faster
|
||||
*/
|
||||
export type ImageQuality = 75 | 85;
|
||||
|
||||
/**
|
||||
* Length of one synodic month in days.
|
||||
* Source: IAU mean value (J2000.0 epoch).
|
||||
*
|
||||
* IAU mean value at J2000.0. Used by `cycleMonth` to divide the elapsed
|
||||
* time into a fractional position within the current lunar cycle.
|
||||
*
|
||||
* @example
|
||||
* import { SYNODIC_MONTH } from 'moon-cycle';
|
||||
* // Days old: how far into the current cycle
|
||||
* const now = new Date();
|
||||
* const elapsed = (now.getTime() - MONTH_ANCHOR.getTime()) / 86400000;
|
||||
* const age = ((elapsed % SYNODIC_MONTH) + SYNODIC_MONTH) % SYNODIC_MONTH;
|
||||
*/
|
||||
export const SYNODIC_MONTH = 29.53058821398858;
|
||||
|
||||
/** Number of images in the monthly (mm) dataset. */
|
||||
/**
|
||||
* Number of images in the monthly (`mm`) dataset.
|
||||
*
|
||||
* Equals the number of hours in one synodic month, rounded to the nearest
|
||||
* integer. Filenames range from `"001.webp"` to `"708.webp"`.
|
||||
*
|
||||
* @example
|
||||
* import { MONTH_IMAGES } from 'moon-cycle';
|
||||
* // Fraction of the way through the synodic cycle
|
||||
* const index = Math.floor(fraction * MONTH_IMAGES) + 1;
|
||||
*/
|
||||
export const MONTH_IMAGES = 708;
|
||||
|
||||
/** Number of images in the yearly (my) dataset. */
|
||||
/**
|
||||
* Number of images in the yearly (`my`) dataset.
|
||||
*
|
||||
* Equals 365 days × 24 hours. Filenames range from `"0001.webp"` to
|
||||
* `"8760.webp"`.
|
||||
*
|
||||
* @example
|
||||
* import { YEAR_IMAGES } from 'moon-cycle';
|
||||
* // Total hours in the yearly dataset
|
||||
* console.log(YEAR_IMAGES); // 8760
|
||||
*/
|
||||
export const YEAR_IMAGES = 8760;
|
||||
|
||||
/**
|
||||
* Anchor date for the monthly cycle: the 2023-11-13 new moon (UTC).
|
||||
* All synodic phase calculations are derived from this reference point.
|
||||
*
|
||||
* All synodic phase calculations measure elapsed time from this reference
|
||||
* point. Confirmed against JPL Horizons ephemeris data.
|
||||
*
|
||||
* @example
|
||||
* import { MONTH_ANCHOR } from 'moon-cycle';
|
||||
* const ageMs = Date.now() - MONTH_ANCHOR.getTime();
|
||||
*/
|
||||
export const MONTH_ANCHOR = new Date('2023-11-13T09:27:00Z');
|
||||
export const MONTH_ANCHOR = new Date("2023-11-13T09:27:00Z");
|
||||
|
||||
/**
|
||||
* Anchor date for the yearly cycle: start of the 2023 NASA image collection.
|
||||
* The 8,760 images correspond to one per hour for the full calendar year 2023.
|
||||
*
|
||||
* The 8,760 images correspond to one per hour for the full calendar year
|
||||
* 2023. `cycleYear` measures elapsed time from this reference point.
|
||||
*
|
||||
* @example
|
||||
* import { YEAR_ANCHOR } from 'moon-cycle';
|
||||
* const hours = (Date.now() - YEAR_ANCHOR.getTime()) / 3600000;
|
||||
*/
|
||||
export const YEAR_ANCHOR = new Date('2023-01-01T00:00:00Z');
|
||||
export const YEAR_ANCHOR = new Date("2023-01-01T00:00:00Z");
|
||||
|
|
|
|||
114
test-cjs.cjs
114
test-cjs.cjs
|
|
@ -3,9 +3,10 @@
|
|||
* Verifies CommonJS compatibility of the built package.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { describe, it } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {
|
||||
cycleMonth,
|
||||
cycleYear,
|
||||
|
|
@ -16,64 +17,59 @@ const {
|
|||
YEAR_IMAGES,
|
||||
MONTH_ANCHOR,
|
||||
YEAR_ANCHOR,
|
||||
} = require('./dist/index.cjs');
|
||||
} = require("./dist/index.cjs");
|
||||
|
||||
let passed = 0;
|
||||
let total = 0;
|
||||
describe("CJS exports", () => {
|
||||
it("all exports are available via require()", () => {
|
||||
assert.strictEqual(typeof cycleMonth, "function");
|
||||
assert.strictEqual(typeof cycleYear, "function");
|
||||
assert.strictEqual(typeof imageFolder, "function");
|
||||
assert.strictEqual(typeof cdnUrl, "function");
|
||||
assert.strictEqual(typeof SYNODIC_MONTH, "number");
|
||||
assert.strictEqual(typeof MONTH_IMAGES, "number");
|
||||
assert.strictEqual(typeof YEAR_IMAGES, "number");
|
||||
assert(MONTH_ANCHOR instanceof Date);
|
||||
assert(YEAR_ANCHOR instanceof Date);
|
||||
});
|
||||
|
||||
function test(name, fn) {
|
||||
total++;
|
||||
try {
|
||||
fn();
|
||||
console.log(`[${name}]... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
it("cycleMonth at anchor returns 001.webp", () => {
|
||||
assert.strictEqual(cycleMonth(MONTH_ANCHOR), "001.webp");
|
||||
});
|
||||
|
||||
test('exports are available via require()', () => {
|
||||
assert.strictEqual(typeof cycleMonth, 'function');
|
||||
assert.strictEqual(typeof cycleYear, 'function');
|
||||
assert.strictEqual(typeof imageFolder, 'function');
|
||||
assert.strictEqual(typeof cdnUrl, 'function');
|
||||
assert.strictEqual(typeof SYNODIC_MONTH, 'number');
|
||||
assert.strictEqual(typeof MONTH_IMAGES, 'number');
|
||||
assert.strictEqual(typeof YEAR_IMAGES, 'number');
|
||||
assert(MONTH_ANCHOR instanceof Date);
|
||||
assert(YEAR_ANCHOR instanceof Date);
|
||||
it("cycleYear at anchor returns 0001.webp", () => {
|
||||
assert.strictEqual(cycleYear(YEAR_ANCHOR), "0001.webp");
|
||||
});
|
||||
|
||||
it("cycleMonth result format is correct", () => {
|
||||
assert.match(cycleMonth(), /^\d{3}\.webp$/);
|
||||
});
|
||||
|
||||
it("cycleYear result format is correct", () => {
|
||||
assert.match(cycleYear(), /^\d{4}\.webp$/);
|
||||
});
|
||||
|
||||
it("imageFolder constructs correct path", () => {
|
||||
assert.strictEqual(imageFolder("mm", 256, 75), "mm-256-75");
|
||||
});
|
||||
|
||||
it("cdnUrl returns expected jsDelivr URL", () => {
|
||||
assert.strictEqual(
|
||||
cdnUrl("001.webp", "mm", 256, 75),
|
||||
"https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/001.webp",
|
||||
);
|
||||
});
|
||||
|
||||
it("cycleMonth throws TypeError for invalid Date", () => {
|
||||
assert.throws(() => cycleMonth(new Date("invalid")), {
|
||||
name: "TypeError",
|
||||
message: "date must be a valid Date instance",
|
||||
});
|
||||
});
|
||||
|
||||
it("cycleYear throws TypeError for invalid Date", () => {
|
||||
assert.throws(() => cycleYear(new Date("invalid")), {
|
||||
name: "TypeError",
|
||||
message: "date must be a valid Date instance",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('cycleMonth at anchor returns 001.webp', () => {
|
||||
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
|
||||
});
|
||||
|
||||
test('cycleYear at anchor returns 0001.webp', () => {
|
||||
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
|
||||
});
|
||||
|
||||
test('cycleMonth result format is correct', () => {
|
||||
const result = cycleMonth();
|
||||
assert.match(result, /^\d{3}\.webp$/);
|
||||
});
|
||||
|
||||
test('cycleYear result format is correct', () => {
|
||||
const result = cycleYear();
|
||||
assert.match(result, /^\d{4}\.webp$/);
|
||||
});
|
||||
|
||||
test('imageFolder constructs correct path', () => {
|
||||
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
|
||||
});
|
||||
|
||||
test('cdnUrl returns expected jsDelivr URL', () => {
|
||||
const url = cdnUrl('001.webp', 'mm', 256, 75);
|
||||
assert.strictEqual(
|
||||
url,
|
||||
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/001.webp'
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`\n${passed}/${total} tests passed`);
|
||||
if (passed < total) process.exit(1);
|
||||
|
|
|
|||
415
test.mjs
415
test.mjs
|
|
@ -1,9 +1,10 @@
|
|||
/**
|
||||
* ESM test suite for moon-cycle v2.
|
||||
* Uses Node.js built-in assert — no test framework required.
|
||||
* Uses Node.js built-in test runner.
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
cycleMonth,
|
||||
cycleYear,
|
||||
|
|
@ -14,227 +15,213 @@ import {
|
|||
YEAR_IMAGES,
|
||||
MONTH_ANCHOR,
|
||||
YEAR_ANCHOR,
|
||||
} from './dist/index.mjs';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
} from "./dist/index.mjs";
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
test('SYNODIC_MONTH is correct IAU value', () => {
|
||||
assert.strictEqual(SYNODIC_MONTH, 29.53058821398858);
|
||||
describe("constants", () => {
|
||||
it("SYNODIC_MONTH is correct IAU value", () => {
|
||||
assert.strictEqual(SYNODIC_MONTH, 29.53058821398858);
|
||||
});
|
||||
|
||||
it("MONTH_IMAGES is 708", () => {
|
||||
assert.strictEqual(MONTH_IMAGES, 708);
|
||||
});
|
||||
|
||||
it("YEAR_IMAGES is 8760", () => {
|
||||
assert.strictEqual(YEAR_IMAGES, 8760);
|
||||
});
|
||||
|
||||
it("MONTH_ANCHOR is 2023-11-13T09:27:00Z", () => {
|
||||
assert.strictEqual(MONTH_ANCHOR.toISOString(), "2023-11-13T09:27:00.000Z");
|
||||
});
|
||||
|
||||
it("YEAR_ANCHOR is 2023-01-01T00:00:00Z", () => {
|
||||
assert.strictEqual(YEAR_ANCHOR.toISOString(), "2023-01-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
test('MONTH_IMAGES is 708', () => {
|
||||
assert.strictEqual(MONTH_IMAGES, 708);
|
||||
// ─── cycleMonth ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe("cycleMonth", () => {
|
||||
it("returns a string", () => {
|
||||
assert.strictEqual(typeof cycleMonth(), "string");
|
||||
});
|
||||
|
||||
it("result matches /^\\d{3}\\.webp$/", () => {
|
||||
assert.match(cycleMonth(), /^\d{3}\.webp$/);
|
||||
});
|
||||
|
||||
it("result is never 000.webp (images are 1-indexed)", () => {
|
||||
const anchor = MONTH_ANCHOR.getTime();
|
||||
const synodic_ms = SYNODIC_MONTH * 24 * 60 * 60 * 1000;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const date = new Date(anchor + i * (synodic_ms / 100));
|
||||
assert.notStrictEqual(cycleMonth(date), "000.webp", `Got 000.webp for offset ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("at anchor date returns 001.webp (start of synodic cycle)", () => {
|
||||
assert.strictEqual(cycleMonth(MONTH_ANCHOR), "001.webp");
|
||||
});
|
||||
|
||||
it("one synodic month + 1 min after anchor returns near start of next cycle", () => {
|
||||
const oneMonthPlus = new Date(
|
||||
MONTH_ANCHOR.getTime() + SYNODIC_MONTH * 24 * 60 * 60 * 1000 + 60_000,
|
||||
);
|
||||
const index = parseInt(cycleMonth(oneMonthPlus).replace(".webp", ""), 10);
|
||||
assert(index <= 3, `Expected near start of next cycle (index <= 3), got ${index}`);
|
||||
});
|
||||
|
||||
it("at halfway through synodic month returns ~354.webp", () => {
|
||||
const half = new Date(MONTH_ANCHOR.getTime() + (SYNODIC_MONTH / 2) * 24 * 60 * 60 * 1000);
|
||||
const index = parseInt(cycleMonth(half).replace(".webp", ""), 10);
|
||||
assert(index >= 353 && index <= 355, `Expected ~354, got ${index}`);
|
||||
});
|
||||
|
||||
it("result is always in range [001, 708]", () => {
|
||||
const start = new Date("2020-01-01T00:00:00Z");
|
||||
const step = 24 * 60 * 60 * 1000 * 7;
|
||||
for (let i = 0; i < 260; i++) {
|
||||
const date = new Date(start.getTime() + i * step);
|
||||
const index = parseInt(cycleMonth(date).replace(".webp", ""), 10);
|
||||
assert(index >= 1, `Index ${index} below minimum (date: ${date.toISOString()})`);
|
||||
assert(index <= 708, `Index ${index} above maximum (date: ${date.toISOString()})`);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles dates before the anchor (pre-2023)", () => {
|
||||
const past = new Date("2020-06-15T00:00:00Z");
|
||||
const result = cycleMonth(past);
|
||||
assert.match(result, /^\d{3}\.webp$/);
|
||||
const index = parseInt(result.replace(".webp", ""), 10);
|
||||
assert(index >= 1 && index <= 708);
|
||||
});
|
||||
|
||||
it("with no args returns a valid result", () => {
|
||||
assert.match(cycleMonth(), /^\d{3}\.webp$/);
|
||||
});
|
||||
|
||||
it("throws TypeError for invalid Date", () => {
|
||||
assert.throws(() => cycleMonth(new Date("invalid")), {
|
||||
name: "TypeError",
|
||||
message: "date must be a valid Date instance",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws TypeError for non-Date input", () => {
|
||||
assert.throws(() => cycleMonth("2023-01-01"), {
|
||||
name: "TypeError",
|
||||
message: "date must be a valid Date instance",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('YEAR_IMAGES is 8760', () => {
|
||||
assert.strictEqual(YEAR_IMAGES, 8760);
|
||||
// ─── cycleYear ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe("cycleYear", () => {
|
||||
it("returns a string", () => {
|
||||
assert.strictEqual(typeof cycleYear(), "string");
|
||||
});
|
||||
|
||||
it("result matches /^\\d{4}\\.webp$/", () => {
|
||||
assert.match(cycleYear(), /^\d{4}\.webp$/);
|
||||
});
|
||||
|
||||
it("result is never 0000.webp (images are 1-indexed)", () => {
|
||||
const anchor = YEAR_ANCHOR.getTime();
|
||||
const year_ms = YEAR_IMAGES * 60 * 60 * 1000;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const date = new Date(anchor + i * (year_ms / 100));
|
||||
assert.notStrictEqual(cycleYear(date), "0000.webp", `Got 0000.webp for offset ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("at anchor date returns 0001.webp (start of year)", () => {
|
||||
assert.strictEqual(cycleYear(YEAR_ANCHOR), "0001.webp");
|
||||
});
|
||||
|
||||
it("at exactly one year after anchor returns 0001.webp", () => {
|
||||
const oneYear = new Date(YEAR_ANCHOR.getTime() + YEAR_IMAGES * 60 * 60 * 1000);
|
||||
assert.strictEqual(cycleYear(oneYear), "0001.webp");
|
||||
});
|
||||
|
||||
it("at halfway through year returns ~4380.webp", () => {
|
||||
const half = new Date(YEAR_ANCHOR.getTime() + (YEAR_IMAGES / 2) * 60 * 60 * 1000);
|
||||
const index = parseInt(cycleYear(half).replace(".webp", ""), 10);
|
||||
assert(index >= 4379 && index <= 4381, `Expected ~4380, got ${index}`);
|
||||
});
|
||||
|
||||
it("result is always in range [0001, 8760]", () => {
|
||||
const start = new Date("2020-01-01T00:00:00Z");
|
||||
const step = 24 * 60 * 60 * 1000 * 7;
|
||||
for (let i = 0; i < 260; i++) {
|
||||
const date = new Date(start.getTime() + i * step);
|
||||
const index = parseInt(cycleYear(date).replace(".webp", ""), 10);
|
||||
assert(index >= 1, `Index ${index} below minimum`);
|
||||
assert(index <= 8760, `Index ${index} above maximum`);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles dates before 2023", () => {
|
||||
const past = new Date("2021-06-15T00:00:00Z");
|
||||
const result = cycleYear(past);
|
||||
assert.match(result, /^\d{4}\.webp$/);
|
||||
const index = parseInt(result.replace(".webp", ""), 10);
|
||||
assert(index >= 1 && index <= 8760);
|
||||
});
|
||||
|
||||
it("with no args returns a valid result", () => {
|
||||
assert.match(cycleYear(), /^\d{4}\.webp$/);
|
||||
});
|
||||
|
||||
it("throws TypeError for invalid Date", () => {
|
||||
assert.throws(() => cycleYear(new Date("invalid")), {
|
||||
name: "TypeError",
|
||||
message: "date must be a valid Date instance",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws TypeError for non-Date input", () => {
|
||||
assert.throws(() => cycleYear("2023-01-01"), {
|
||||
name: "TypeError",
|
||||
message: "date must be a valid Date instance",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('MONTH_ANCHOR is 2023-11-13T09:27:00Z', () => {
|
||||
assert.strictEqual(MONTH_ANCHOR.toISOString(), '2023-11-13T09:27:00.000Z');
|
||||
// ─── imageFolder ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe("imageFolder", () => {
|
||||
it("returns correct folder name", () => {
|
||||
assert.strictEqual(imageFolder("mm", 256, 75), "mm-256-75");
|
||||
assert.strictEqual(imageFolder("mm", 512, 85), "mm-512-85");
|
||||
assert.strictEqual(imageFolder("my", 256, 75), "my-256-75");
|
||||
assert.strictEqual(imageFolder("my", 512, 85), "my-512-85");
|
||||
});
|
||||
});
|
||||
|
||||
test('YEAR_ANCHOR is 2023-01-01T00:00:00Z', () => {
|
||||
assert.strictEqual(YEAR_ANCHOR.toISOString(), '2023-01-01T00:00:00.000Z');
|
||||
// ─── cdnUrl ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("cdnUrl", () => {
|
||||
it("returns a valid jsDelivr URL", () => {
|
||||
assert.strictEqual(
|
||||
cdnUrl("354.webp", "mm", 256, 75),
|
||||
"https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp",
|
||||
);
|
||||
});
|
||||
|
||||
it("respects custom ref parameter", () => {
|
||||
assert.strictEqual(
|
||||
cdnUrl("001.webp", "my", 512, 85, "v2.0.0"),
|
||||
"https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@v2.0.0/my-512-85/001.webp",
|
||||
);
|
||||
});
|
||||
|
||||
it("integrates with cycleMonth output", () => {
|
||||
const filename = cycleMonth(MONTH_ANCHOR);
|
||||
const url = cdnUrl(filename, "mm", 256, 75);
|
||||
assert(url.startsWith("https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/"));
|
||||
assert(url.endsWith(".webp"));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── cycleMonth: return format ────────────────────────────────────────────────
|
||||
|
||||
test('cycleMonth returns a string', () => {
|
||||
assert.strictEqual(typeof cycleMonth(), 'string');
|
||||
});
|
||||
|
||||
test('cycleMonth result matches /^\\d{3}\\.webp$/', () => {
|
||||
const result = cycleMonth();
|
||||
assert.match(result, /^\d{3}\.webp$/);
|
||||
});
|
||||
|
||||
test('cycleMonth result is never 000.webp (images are 1-indexed)', () => {
|
||||
// Test many dates across different lunar phases
|
||||
const anchor = MONTH_ANCHOR.getTime();
|
||||
const synodic_ms = SYNODIC_MONTH * 24 * 60 * 60 * 1000;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const date = new Date(anchor + i * (synodic_ms / 100));
|
||||
const result = cycleMonth(date);
|
||||
assert.notStrictEqual(result, '000.webp', `Got 000.webp for offset ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── cycleMonth: anchor date ──────────────────────────────────────────────────
|
||||
|
||||
test('cycleMonth at anchor date returns 001.webp (start of synodic cycle)', () => {
|
||||
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
|
||||
});
|
||||
|
||||
test('cycleMonth one synodic month + 1 min after anchor returns near start of next cycle', () => {
|
||||
// Adding exactly one synodic month in floating-point can land on either side of the
|
||||
// cycle boundary (001 or 708) due to IEEE 754 rounding. Adding 1 minute steps past it.
|
||||
const oneMonthPlus = new Date(
|
||||
MONTH_ANCHOR.getTime() + SYNODIC_MONTH * 24 * 60 * 60 * 1000 + 60_000
|
||||
);
|
||||
const result = cycleMonth(oneMonthPlus);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
assert(index <= 3, `Expected near start of next cycle (index <= 3), got ${index}`);
|
||||
});
|
||||
|
||||
test('cycleMonth at halfway through synodic month returns ~354.webp', () => {
|
||||
const half = new Date(
|
||||
MONTH_ANCHOR.getTime() + (SYNODIC_MONTH / 2) * 24 * 60 * 60 * 1000
|
||||
);
|
||||
const result = cycleMonth(half);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
// Allow ±1 for rounding
|
||||
assert(index >= 353 && index <= 355, `Expected ~354, got ${index}`);
|
||||
});
|
||||
|
||||
test('cycleMonth result is always in range [001, 708]', () => {
|
||||
// Test dates spanning 5 years
|
||||
const start = new Date('2020-01-01T00:00:00Z');
|
||||
const step = 24 * 60 * 60 * 1000 * 7; // weekly
|
||||
for (let i = 0; i < 260; i++) {
|
||||
const date = new Date(start.getTime() + i * step);
|
||||
const result = cycleMonth(date);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
assert(index >= 1, `Index ${index} below minimum (date: ${date.toISOString()})`);
|
||||
assert(index <= 708, `Index ${index} above maximum (date: ${date.toISOString()})`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── cycleMonth: past dates ───────────────────────────────────────────────────
|
||||
|
||||
test('cycleMonth handles dates before the anchor (pre-2023)', () => {
|
||||
const past = new Date('2020-06-15T00:00:00Z');
|
||||
const result = cycleMonth(past);
|
||||
assert.match(result, /^\d{3}\.webp$/);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
assert(index >= 1 && index <= 708);
|
||||
});
|
||||
|
||||
// ─── cycleYear: return format ─────────────────────────────────────────────────
|
||||
|
||||
test('cycleYear returns a string', () => {
|
||||
assert.strictEqual(typeof cycleYear(), 'string');
|
||||
});
|
||||
|
||||
test('cycleYear result matches /^\\d{4}\\.webp$/', () => {
|
||||
const result = cycleYear();
|
||||
assert.match(result, /^\d{4}\.webp$/);
|
||||
});
|
||||
|
||||
test('cycleYear result is never 0000.webp (images are 1-indexed)', () => {
|
||||
const anchor = YEAR_ANCHOR.getTime();
|
||||
const year_ms = YEAR_IMAGES * 60 * 60 * 1000;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const date = new Date(anchor + i * (year_ms / 100));
|
||||
const result = cycleYear(date);
|
||||
assert.notStrictEqual(result, '0000.webp', `Got 0000.webp for offset ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── cycleYear: anchor date ───────────────────────────────────────────────────
|
||||
|
||||
test('cycleYear at anchor date returns 0001.webp (start of year)', () => {
|
||||
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
|
||||
});
|
||||
|
||||
test('cycleYear at exactly one year after anchor returns 0001.webp', () => {
|
||||
const oneYear = new Date(
|
||||
YEAR_ANCHOR.getTime() + YEAR_IMAGES * 60 * 60 * 1000
|
||||
);
|
||||
assert.strictEqual(cycleYear(oneYear), '0001.webp');
|
||||
});
|
||||
|
||||
test('cycleYear at halfway through year returns ~4380.webp', () => {
|
||||
const half = new Date(
|
||||
YEAR_ANCHOR.getTime() + (YEAR_IMAGES / 2) * 60 * 60 * 1000
|
||||
);
|
||||
const result = cycleYear(half);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
assert(index >= 4379 && index <= 4381, `Expected ~4380, got ${index}`);
|
||||
});
|
||||
|
||||
test('cycleYear result is always in range [0001, 8760]', () => {
|
||||
const start = new Date('2020-01-01T00:00:00Z');
|
||||
const step = 24 * 60 * 60 * 1000 * 7;
|
||||
for (let i = 0; i < 260; i++) {
|
||||
const date = new Date(start.getTime() + i * step);
|
||||
const result = cycleYear(date);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
assert(index >= 1, `Index ${index} below minimum`);
|
||||
assert(index <= 8760, `Index ${index} above maximum`);
|
||||
}
|
||||
});
|
||||
|
||||
test('cycleYear handles dates before 2023', () => {
|
||||
const past = new Date('2021-06-15T00:00:00Z');
|
||||
const result = cycleYear(past);
|
||||
assert.match(result, /^\d{4}\.webp$/);
|
||||
const index = parseInt(result.replace('.webp', ''), 10);
|
||||
assert(index >= 1 && index <= 8760);
|
||||
});
|
||||
|
||||
// ─── cycleMonth/cycleYear default parameter ───────────────────────────────────
|
||||
|
||||
test('cycleMonth() with no args returns a valid result', () => {
|
||||
const result = cycleMonth();
|
||||
assert.match(result, /^\d{3}\.webp$/);
|
||||
});
|
||||
|
||||
test('cycleYear() with no args returns a valid result', () => {
|
||||
const result = cycleYear();
|
||||
assert.match(result, /^\d{4}\.webp$/);
|
||||
});
|
||||
|
||||
// ─── imageFolder ──────────────────────────────────────────────────────────────
|
||||
|
||||
test('imageFolder returns correct folder name', () => {
|
||||
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
|
||||
assert.strictEqual(imageFolder('mm', 512, 85), 'mm-512-85');
|
||||
assert.strictEqual(imageFolder('my', 256, 75), 'my-256-75');
|
||||
assert.strictEqual(imageFolder('my', 512, 85), 'my-512-85');
|
||||
});
|
||||
|
||||
// ─── cdnUrl ───────────────────────────────────────────────────────────────────
|
||||
|
||||
test('cdnUrl returns a valid jsDelivr URL', () => {
|
||||
const url = cdnUrl('354.webp', 'mm', 256, 75);
|
||||
assert.strictEqual(
|
||||
url,
|
||||
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
|
||||
);
|
||||
});
|
||||
|
||||
test('cdnUrl respects custom ref parameter', () => {
|
||||
const url = cdnUrl('001.webp', 'my', 512, 85, 'v2.0.0');
|
||||
assert.strictEqual(
|
||||
url,
|
||||
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@v2.0.0/my-512-85/001.webp'
|
||||
);
|
||||
});
|
||||
|
||||
test('cdnUrl integrates with cycleMonth output', () => {
|
||||
const filename = cycleMonth(MONTH_ANCHOR);
|
||||
const url = cdnUrl(filename, 'mm', 256, 75);
|
||||
assert(url.startsWith('https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/'));
|
||||
assert(url.endsWith('.webp'));
|
||||
});
|
||||
|
||||
// ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\n${passed}/${total} tests passed`);
|
||||
if (passed < total) process.exit(1);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
{
|
||||
"extends": "@acamarata/tsconfig/tsconfig.library.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
outDir: 'dist',
|
||||
outDir: "dist",
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
target: 'es2020',
|
||||
platform: 'neutral',
|
||||
outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.mjs' }),
|
||||
target: "es2020",
|
||||
platform: "neutral",
|
||||
outExtension: ({ format }) => ({ js: format === "cjs" ? ".cjs" : ".mjs" }),
|
||||
});
|
||||
|
|
|
|||
10
typedoc.json
Normal file
10
typedoc.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"out": ".github/wiki/api",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"readme": "none",
|
||||
"skipErrorChecking": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"includeVersion": true
|
||||
}
|
||||
Loading…
Reference in a new issue