From b44d9a958b5460cfd2057fa56e896401e082c386 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 25 Feb 2026 11:01:38 -0500 Subject: [PATCH] v2.0.0: TypeScript rewrite with dual CJS/ESM build Complete modernization of the package. The core SPA algorithm is unchanged and validated; everything else is rebuilt to match current JavaScript ecosystem standards. Changes: - TypeScript wrapper in src/ with full type definitions - Dual CJS/ESM build via tsup (dist/index.cjs, dist/index.mjs) - Core algorithm moved from dist/spa.js to lib/spa.js (same code) - Input validation with descriptive TypeError/RangeError messages - formatTime() and SPA function code constants as named exports - getSpa() / calcSpa() accept null for optional args (tz, options) - Test suite: 61 ESM assertions and 17 CJS assertions - GitHub Actions CI: Node 20/22/24 matrix, typecheck, pack-check - GitHub Wiki: Home, API Reference, Architecture, Twilight, NREL SPA - NREL attribution in LICENSE and README per their license terms - package.json: exports map, files, engines >=20, sideEffects: false - Author corrected to Aric Camarata; repository.url uses git+https:// - LICENSE year corrected to 2023-2026 - Removed: index.js, test.js, dist/spa.js (superseded by above) --- .DS_Store | Bin 6148 -> 0 bytes .editorconfig | 14 + .github/workflows/ci.yml | 78 +++ .github/workflows/wiki-sync.yml | 36 ++ .gitignore | 26 +- .npmrc | 1 + .nvmrc | 1 + .wiki/API-Reference.md | 131 +++++ .wiki/Architecture.md | 83 +++ .wiki/Home.md | 44 ++ .wiki/NREL-SPA-Algorithm.md | 76 +++ .wiki/Twilight-Calculations.md | 89 +++ CHANGELOG.md | 58 +- LICENSE | 46 +- README.md | 190 ++++-- index.js | 118 ---- lib/spa.js | 989 ++++++++++++++++++++++++++++++++ package.json | 64 ++- pnpm-lock.yaml | 925 +++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + src/index.ts | 240 ++++++++ src/types.ts | 71 +++ test-cjs.cjs | 76 +++ test.js | 41 -- test.mjs | 257 +++++++++ tsconfig.json | 20 + tsup.config.ts | 30 + 27 files changed, 3478 insertions(+), 228 deletions(-) delete mode 100644 .DS_Store create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/wiki-sync.yml create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .wiki/API-Reference.md create mode 100644 .wiki/Architecture.md create mode 100644 .wiki/Home.md create mode 100644 .wiki/NREL-SPA-Algorithm.md create mode 100644 .wiki/Twilight-Calculations.md delete mode 100644 index.js create mode 100644 lib/spa.js create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 src/index.ts create mode 100644 src/types.ts create mode 100644 test-cjs.cjs delete mode 100644 test.js create mode 100644 test.mjs create mode 100644 tsconfig.json create mode 100644 tsup.config.ts diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 64a1d28e6378bfb701c627c671b2326c8a17aecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!HN?>5Uox|?YIg#DD1JpYtWsI=z^Cq#vgEGJ*dP?CM1KC>5!S2LkP@K^zL~- z!;1$&`~h*{{hD47dZWmO)o~o|u>DQU=6aa|MJoW*a03cBby{p*l5b~35 zNsTSn5C!E(M}9calj+t-XK)NS2L43`=-c(+HjE&FqWb-w{n+$PbhjI&IzpQa$Aj;m zzkGdI^~ZnoPsbZ)L+u1Zp$-wwnnjCz%9%e#(=^G8gM(91t9zF&Uy)Mo$(P|+FT$c< z%=1ov_MD!cYMn;wem^=+hKpYF#v`2-{UjUCOh6J3amb5jNfzrxN6)i3H?g60$VQ{l zYwjTJv1A^KGhNwqZk9m z(jt0LgbhWsp@K^cVZ+g`cwE3(TD0LHxcCq}v)~FvnAtJEGQ&Xx7G3KYa12x#sN2UT z-T#k&U;kHw+>>L#G4Nk8AiQqa?ckF5ZrxfO-L(q!6)FjhD=nH3Z1_4h9lDD5QH`Ka YQ3o+#EG?o1#r+X5G`Pkw@K+i53B-<#$N&HU diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9220599 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{js,mjs,cjs,ts,mts,cts,json,yml,yaml,md}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..11a07b4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22, 24] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Build TypeScript + run: pnpm run build + + - name: Run tests (ESM) + run: node test.mjs + + - name: Run tests (CJS) + run: node test-cjs.cjs + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run typecheck + + pack-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run build + + - name: Verify package contents + run: | + npm pack --dry-run 2>&1 | tee pack-output.txt + for f in dist/index.cjs dist/index.mjs dist/index.d.ts dist/index.d.mts lib/spa.js README.md CHANGELOG.md LICENSE; do + grep -q "$f" pack-output.txt || { echo "MISSING: $f"; exit 1; } + done + echo "All expected files present in package" diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml new file mode 100644 index 0000000..50ef085 --- /dev/null +++ b/.github/workflows/wiki-sync.yml @@ -0,0 +1,36 @@ +name: Sync Wiki + +on: + push: + branches: [main] + paths: ['.wiki/**'] + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Checkout wiki + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }}.wiki + path: .wiki-remote + + - name: Sync wiki pages + run: | + cp .wiki/*.md .wiki-remote/ + cd .wiki-remote + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "No wiki changes to commit" + else + git commit -m "Sync wiki from repo" + git push + fi diff --git a/.gitignore b/.gitignore index a733e7e..a969533 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,28 @@ node_modules/ -.env +dist/ +*.tgz +*.log +.DS_Store -# Ignore NREL SPA C sources and binaries +# Environment +.env +.env.* +!.env.example + +# AI Agents +.claude/ +.cursor/ +.copilot/ +.github/copilot/ +.aider* +.codeium/ +.tabnine/ +.windsurf/ +.cody/ +.sourcegraph/ + +# NREL SPA C sources and binaries (download separately for reference testing) /bin/spa /bin/spa_cli /bin/*.c -/bin/*.h \ No newline at end of file +/bin/*.h diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..391eb15 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-import-method=hardlink diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.wiki/API-Reference.md b/.wiki/API-Reference.md new file mode 100644 index 0000000..46ce9f8 --- /dev/null +++ b/.wiki/API-Reference.md @@ -0,0 +1,131 @@ +# API Reference + +## Functions + +### `getSpa(date, latitude, longitude, timezone?, options?, angles?)` + +Computes solar position for the given date and location. Returns raw numerical values. + +**Parameters:** + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `date` | `Date` | Yes | UTC date and time. Uses UTC components internally. | +| `latitude` | `number` | Yes | Observer latitude, -90 to 90. Negative = south. | +| `longitude` | `number` | Yes | Observer longitude, -180 to 180. Negative = west. | +| `timezone` | `number \| null` | No | Hours from UTC (e.g., -4 for EDT). Default: `0`. | +| `options` | `SpaOptions \| null` | No | Atmospheric and calculation parameters. | +| `angles` | `number[]` | No | Custom zenith angles in degrees. See [Twilight Calculations](Twilight-Calculations). | + +**Returns:** `SpaResult` + +```typescript +interface SpaResult { + zenith: number; // topocentric zenith angle (degrees) + azimuth: number; // topocentric azimuth, eastward from north (degrees) + sunrise: number; // local sunrise (fractional hours, e.g. 5.417 = 05:25) + solarNoon: number; // local solar noon (fractional hours) + sunset: number; // local sunset (fractional hours) +} +``` + +When `angles` is provided, returns `SpaResultWithAngles`: + +```typescript +interface SpaResultWithAngles extends SpaResult { + angles: Array<{ sunrise: number; sunset: number }>; +} +``` + +**Throws:** +- `TypeError` if `date` is not a valid Date, or `latitude`/`longitude` are not finite numbers +- `RangeError` if `latitude` is outside [-90, 90] or `longitude` outside [-180, 180] +- `Error` if the internal SPA calculation returns a non-zero error code + +--- + +### `calcSpa(date, latitude, longitude, timezone?, options?, angles?)` + +Same parameters as `getSpa()`. Formats `sunrise`, `solarNoon`, and `sunset` as `HH:MM:SS` strings. + +**Returns:** `SpaFormattedResult` + +```typescript +interface SpaFormattedResult { + zenith: number; // same as SpaResult + azimuth: number; // same as SpaResult + sunrise: string; // "HH:MM:SS" or "N/A" during polar day/night + solarNoon: string; // "HH:MM:SS" or "N/A" + sunset: string; // "HH:MM:SS" or "N/A" +} +``` + +When `angles` is provided, returns `SpaFormattedResultWithAngles`: + +```typescript +interface SpaFormattedResultWithAngles extends SpaFormattedResult { + angles: Array<{ sunrise: string; sunset: string }>; +} +``` + +--- + +### `formatTime(hours)` + +Converts a fractional hour value to `HH:MM:SS` format. + +```javascript +formatTime(5.417489) // "05:25:03" +formatTime(12) // "12:00:00" +formatTime(23.9997) // "23:59:59" +formatTime(-1) // "N/A" +formatTime(NaN) // "N/A" +formatTime(Infinity) // "N/A" +``` + +--- + +## SpaOptions + +All options are optional. Defaults match the NREL C reference implementation. + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `elevation` | `number` | `0` | Observer elevation above sea level in meters. | +| `pressure` | `number` | `1013` | Annual average atmospheric pressure in millibars. | +| `temperature` | `number` | `15` | Annual average temperature in degrees Celsius. | +| `delta_ut1` | `number` | `0` | Fractional second difference between UTC and UT1. Valid range: (-1, 1). | +| `delta_t` | `number` | `67` | Difference between Terrestrial Time and UTC in seconds. | +| `slope` | `number` | `0` | Surface slope from horizontal in degrees. Used for incidence angle (SPA_ZA_INC, SPA_ALL). | +| `azm_rotation` | `number` | `0` | Surface azimuth rotation from south in degrees. | +| `atmos_refract` | `number` | `0.5667` | Atmospheric refraction at sunrise/sunset in degrees. | +| `function` | `SpaFunctionCode` | `SPA_ZA_RTS` | Which outputs to compute. | + +--- + +## Function Codes + +```javascript +import { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from 'nrel-spa'; +``` + +| Constant | Value | Outputs Computed | +| --- | --- | --- | +| `SPA_ZA` | `0` | `zenith`, `azimuth` | +| `SPA_ZA_INC` | `1` | `zenith`, `azimuth`, incidence angle (for solar panels on sloped surfaces) | +| `SPA_ZA_RTS` | `2` | `zenith`, `azimuth`, `sunrise`, `solarNoon`, `sunset` | +| `SPA_ALL` | `3` | All outputs from SPA_ZA_INC and SPA_ZA_RTS combined | + +The default is `SPA_ZA_RTS`. Use `SPA_ZA` for zenith/azimuth-only calculations if you do not need rise/set times; it skips the three-day calculation that rise/set requires and is slightly faster. + +--- + +## SpaFunctionCode Type + +```typescript +type SpaFunctionCode = 0 | 1 | 2 | 3; +``` + +--- + +[Home](Home) . [Architecture](Architecture) . [Twilight Calculations](Twilight-Calculations) . [NREL SPA Algorithm](NREL-SPA-Algorithm) diff --git a/.wiki/Architecture.md b/.wiki/Architecture.md new file mode 100644 index 0000000..6013abd --- /dev/null +++ b/.wiki/Architecture.md @@ -0,0 +1,83 @@ +# Architecture + +## Package Structure + +``` +nrel-spa/ +├── src/ +│ ├── index.ts <- Public API: getSpa, calcSpa, formatTime +│ └── types.ts <- TypeScript interfaces and SPA function code constants +├── lib/ +│ └── spa.js <- Core SPA algorithm (JS port of NREL C source, tracked in git) +├── dist/ <- Built output (generated by tsup, gitignored) +│ ├── index.cjs <- CommonJS build +│ ├── index.mjs <- ESM build +│ ├── index.d.ts <- CJS type declarations +│ └── index.d.mts <- ESM type declarations +├── test.mjs <- ESM test suite (61 assertions) +└── test-cjs.cjs <- CJS test suite (17 assertions) +``` + +## Layers + +**lib/spa.js** is the core mathematical engine. It is a direct JavaScript port of the NREL C source (`spa.c`), preserving function names, variable names, and the algorithm structure. The file is checked into git and shipped in the npm package. + +**src/index.ts** is a thin TypeScript wrapper. It: +- Loads `lib/spa.js` at runtime +- Validates input parameters, throwing `TypeError` or `RangeError` for invalid values +- Maps the flat SpaData structure to a clean output object +- Implements `adjustForCustomAngle()` for twilight calculations +- Provides `formatTime()` as a standalone export + +**tsup** compiles `src/index.ts` to both CJS (`dist/index.cjs`) and ESM (`dist/index.mjs`) with TypeScript declarations. The `lib/spa.js` module is kept external (not bundled) and resolved at runtime via a `createRequire` shim in the ESM build. + +## Loading Strategy + +The ESM build uses a `createRequire` banner injected by tsup: + +```javascript +import { createRequire as __cr } from 'node:module'; +const __require = __cr(import.meta.url); +``` + +This anchors `require()` resolution to the file's own location rather than the calling context, so `../lib/spa.js` resolves correctly regardless of where the caller imports the package. + +In the CJS build, standard `require()` is used directly. + +## Why lib/spa.js Is External + +Unlike a typical npm package where all source is compiled away, `lib/spa.js` ships as a separate file rather than being inlined into `dist/`. This is an intentional choice: + +1. The algorithm is the canonical reference. Keeping it as a readable JS file allows inspection without source maps. +2. The file is large (989 lines). Bundling it into both CJS and ESM outputs would double its footprint for no functional gain. +3. It matches the structure of [solar-spa](https://www.npmjs.com/package/solar-spa), which keeps its Emscripten WASM module in a separate `wasm/` directory. + +## Date Handling + +The wrapper uses UTC components from the `Date` object (`getUTCFullYear()`, etc.) and accepts an explicit `timezone` offset. This avoids the ambiguity of `getFullYear()` which returns local time and can produce wrong results when the caller's timezone differs from the target location. + +```javascript +// Always pass UTC date + explicit timezone +const date = new Date('2025-06-21T00:00:00Z'); +getSpa(date, lat, lng, -4); // -4 for EDT +``` + +## Polar Conditions + +When the sun does not rise or set (polar day/polar night), `lib/spa.js` sets sunrise, sunset, and suntransit to `-99999`. The wrapper propagates these raw values from `getSpa()`. `calcSpa()` passes them through `formatTime()`, which returns `"N/A"` for negative values. + +Custom angle calculations (`adjustForCustomAngle`) return `NaN` sunrise/sunset when `cosH0 < -1 || cosH0 > 1`, which indicates no crossing of that zenith angle. + +## Validation + +Input validation runs before any SPA calculation: + +- `date` must be a valid `Date` (not `Invalid Date`) +- `latitude` and `longitude` must be finite numbers in their valid ranges +- Invalid values throw before the SPA data structure is populated + +The internal `spa_calculate()` function also validates its inputs and returns non-zero error codes for out-of-range values. The wrapper throws `Error` on any non-zero return. + +--- + +[Home](Home) . [API Reference](API-Reference) . [Twilight Calculations](Twilight-Calculations) . [NREL SPA Algorithm](NREL-SPA-Algorithm) diff --git a/.wiki/Home.md b/.wiki/Home.md new file mode 100644 index 0000000..c0c8645 --- /dev/null +++ b/.wiki/Home.md @@ -0,0 +1,44 @@ +# nrel-spa + +Pure JavaScript implementation of the NREL Solar Position Algorithm (SPA). Computes solar zenith angle, azimuth, sunrise, sunset, and solar noon for any location and date. Validated to produce identical output to the original NREL C reference implementation. + +## Overview + +**Package:** [nrel-spa on npm](https://www.npmjs.com/package/nrel-spa) +**Repository:** [acamarata/nrel-spa on GitHub](https://github.com/acamarata/nrel-spa) +**License:** MIT (wrapper). NREL SPA C source: see LICENSE for third-party notice. + +## Pages + +- [API Reference](API-Reference) - Full function signatures, parameters, return types +- [Architecture](Architecture) - How the algorithm is structured and validated +- [Twilight Calculations](Twilight-Calculations) - Custom zenith angles for civil, nautical, astronomical twilight +- [NREL SPA Algorithm](NREL-SPA-Algorithm) - The algorithm background, accuracy, and reference + +## Quick Example + +```javascript +import { calcSpa } from 'nrel-spa'; + +const result = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, // New York latitude + -74.006, // New York longitude + -4, // EDT (UTC-4) +); + +console.log(result.sunrise); // "05:25:03" +console.log(result.solarNoon); // "12:57:56" +console.log(result.sunset); // "20:30:35" +``` + +## Key Facts + +- Zero runtime dependencies +- Synchronous: no async, no WASM, no loading delay +- Dual CJS and ESM, full TypeScript definitions +- Matches NREL C reference output within one second across all tested locations + +--- + +[API Reference](API-Reference) . [Architecture](Architecture) . [Twilight Calculations](Twilight-Calculations) . [NREL SPA Algorithm](NREL-SPA-Algorithm) diff --git a/.wiki/NREL-SPA-Algorithm.md b/.wiki/NREL-SPA-Algorithm.md new file mode 100644 index 0000000..6ab1843 --- /dev/null +++ b/.wiki/NREL-SPA-Algorithm.md @@ -0,0 +1,76 @@ +# NREL SPA Algorithm + +## Background + +The Solar Position Algorithm (SPA) was developed by Ibrahim Reda and Afshin Andreas at the National Renewable Energy Laboratory (NREL) and published in 2004. It is the reference algorithm for solar position calculation in scientific and engineering applications. + +**Citation:** +> Reda, I., Andreas, A. (2004). "Solar Position Algorithm for Solar Radiation Applications." Solar Energy, 76(5), 577-589. https://doi.org/10.1016/j.solener.2003.12.003 + +**Original C source:** https://midcdmz.nrel.gov/spa/ + +The C source has not changed since its last release in September 2014. This JavaScript port is validated against that version. + +## Accuracy + +The algorithm achieves uncertainty of +/- 0.0003 degrees in solar zenith and azimuth angles. For sunrise, solar noon, and sunset, results match the C reference to within one second across all tested locations and dates. + +Validation test cases from `bin/test.js`: + +| Location | Date | Sunrise (C) | Sunrise (JS) | Noon (C) | Noon (JS) | Sunset (C) | Sunset (JS) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| New York | 2025-06-21 | 05:25:03 | 05:25:03 | 12:57:56 | 12:57:56 | 20:30:35 | 20:30:35 | +| New York | 2025-12-21 | 07:16:41 | 07:16:41 | 11:54:19 | 11:54:19 | 16:31:56 | 16:31:56 | +| London | 2025-06-21 | 04:43:07 | 04:43:07 | 13:02:22 | 13:02:22 | 21:21:37 | 21:21:37 | +| London | 2025-12-21 | 08:03:52 | 08:03:52 | 11:58:42 | 11:58:42 | 15:53:32 | 15:53:32 | +| Tokyo | 2025-06-21 | 04:25:52 | 04:25:52 | 11:43:00 | 11:43:00 | 19:00:22 | 19:00:22 | +| Sydney | 2025-06-21 | 07:00:12 | 07:00:12 | 11:56:56 | 11:56:56 | 16:53:52 | 16:53:52 | +| Reykjavik | 2025-06-21 | 02:55:10 | 02:55:10 | 13:29:38 | 13:29:38 | 00:03:54 | 00:03:54 | +| Cape Town | 2025-12-21 | 05:31:55 | 05:31:55 | 12:44:28 | 12:44:28 | 19:57:01 | 19:57:01 | +| Quito | 2025-03-20 | 06:17:54 | 06:17:54 | 12:21:10 | 12:21:10 | 18:24:25 | 18:24:25 | +| Tromso | 2025-12-21 | N/A | N/A | N/A | N/A | N/A | N/A | + +Zero drift in all cases with sun (Tromso is polar night in December). + +## Algorithm Outline + +The SPA computes solar position through a chain of coordinate transformations: + +1. **Julian Day (JD)** from the input date and time, accounting for the timezone offset and DUT1 correction. + +2. **Earth Heliocentric Coordinates** using Variations Seculaires des Orbites Planetaires (VSOP87) truncated to 63 periodic terms for longitude (L), 5 terms for latitude (B), and 40 terms for radius (R). + +3. **Geocentric Coordinates** by converting heliocentric L to geocentric longitude (theta) and negating the latitude (beta = -B). + +4. **Nutation** in longitude (del_psi) and obliquity (del_epsilon) from 63 periodic terms in the IAU 1980 nutation model. + +5. **Apparent Sun Longitude** by adding aberration correction and nutation to theta. + +6. **Greenwich Sidereal Time** and then the **Observer Hour Angle** (H). + +7. **Topocentric Correction** using the observer's elevation, converting geocentric right ascension and declination to topocentric values (alpha_prime, delta_prime, h_prime). + +8. **Zenith and Azimuth** from topocentric elevation angle corrected for atmospheric refraction. + +9. **Rise/Transit/Set** via a three-day calculation: the algorithm solves for sunrise, solar noon, and sunset by interpolating right ascension and declination across the previous, current, and next day, then iterating to correct the approximate times. + +## Key Parameters + +**delta_t** (TT-UTC, default 67 seconds): the accumulated difference between Terrestrial Time (an ideal clock) and UTC (subject to leap seconds). The NREL bulletin value for 2025 is approximately 68-70 seconds. The default of 67 is suitable for dates within a few years of 2020. + +**delta_ut1** (UT1-UTC, default 0): a sub-second correction published by the IERS. For most applications, the default of 0 is acceptable. + +**atmos_refract** (default 0.5667 degrees): atmospheric refraction at the horizon. This shifts the apparent sunrise earlier and sunset later than geometric calculations. The NREL default matches standard atmospheric conditions. + +## Comparison with solar-spa + +[solar-spa](https://www.npmjs.com/package/solar-spa) compiles the same NREL C source to WebAssembly via Emscripten. The two packages share the same algorithm and produce the same results. The practical difference: + +- **nrel-spa** is synchronous, has no loading delay, and is simpler to use in most contexts +- **solar-spa** is asynchronous (WASM initialization), but can achieve higher throughput for batch calculations (219,000 calls/sec for zenith-only vs. nrel-spa's ~100,000/sec) + +For single-call or per-request use cases, nrel-spa is the better choice. For batch pre-computation of thousands of time steps, solar-spa's WASM throughput becomes relevant. + +--- + +[Home](Home) . [API Reference](API-Reference) . [Architecture](Architecture) . [Twilight Calculations](Twilight-Calculations) diff --git a/.wiki/Twilight-Calculations.md b/.wiki/Twilight-Calculations.md new file mode 100644 index 0000000..e9b2e8f --- /dev/null +++ b/.wiki/Twilight-Calculations.md @@ -0,0 +1,89 @@ +# Twilight Calculations + +Standard sunrise and sunset use a zenith angle of approximately 90.833 degrees (90 degrees plus atmospheric refraction and solar disc radius). Twilight is defined by the sun's position below the horizon, expressed as the zenith angle it occupies. + +## Standard Twilight Definitions + +| Type | Zenith Angle | Description | +| --- | --- | --- | +| Sunrise / Sunset | ~90.833 | Center of sun at horizon (accounting for refraction) | +| Civil twilight | 96 | Sufficient light for outdoor activities without artificial light | +| Nautical twilight | 102 | Horizon visible; used for celestial navigation | +| Astronomical twilight | 108 | Sky fully dark enough for astronomical observation | + +## Usage + +Pass an array of zenith angles as the sixth argument to `getSpa()` or `calcSpa()`. The function returns an `angles` array, one entry per input angle, each with `sunrise` and `sunset` for that zenith threshold. + +```javascript +import { calcSpa } from 'nrel-spa'; + +const result = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, // New York + -74.006, + -4, // EDT + {}, // default options + [96, 102, 108], // civil, nautical, astronomical +); + +console.log(result.sunrise); // "05:25:03" (standard) +console.log(result.angles[0].sunrise); // civil twilight begin +console.log(result.angles[1].sunrise); // nautical twilight begin +console.log(result.angles[2].sunrise); // astronomical twilight begin +``` + +Expected output for New York, June 21, 2025: + +| Event | Time | +| --- | --- | +| Astronomical twilight begin | ~03:57 | +| Nautical twilight begin | ~04:28 | +| Civil twilight begin | ~04:53 | +| Sunrise | ~05:25 | +| Sunset | ~20:30 | +| Civil twilight end | ~21:02 | +| Nautical twilight end | ~21:27 | +| Astronomical twilight end | ~21:58 | + +## How It Works + +The base `getSpa()` calculation computes the sun's declination (`delta`) and solar noon (`suntransit`) for the given date and location. For each custom zenith angle `Z`, the function solves the hour angle equation: + +``` +cos(H0) = (cos(Z) - sin(lat) * sin(delta)) / (cos(lat) * cos(delta)) +``` + +where `H0` is the hour angle at the zenith crossing. The rise and set times follow from: + +``` +sunrise = suntransit - H0 / 15 (H0 in degrees, result in hours) +sunset = suntransit + H0 / 15 +``` + +If `|cos(H0)| > 1`, the sun never crosses that zenith angle at the given latitude and date. The function returns `NaN` for sunrise and sunset in that case. + +## Polar Cases + +At high latitudes during summer (midnight sun), the sun may not set even at the standard horizon. It certainly will not cross deeper zenith angles like 96 or 102 degrees. Check for `NaN` or `isFinite()` on the result: + +```javascript +import { getSpa } from 'nrel-spa'; + +const r = getSpa( + new Date('2025-06-21T00:00:00Z'), + 71, 25, 2, {}, [96], +); + +if (r.angles && !isFinite(r.angles[0].sunrise)) { + console.log('No civil twilight at this latitude/date'); +} +``` + +## Islamic Prayer Application + +This twilight mechanism is used by [pray-calc](https://www.npmjs.com/package/pray-calc) to compute Fajr and Isha prayer times, which are defined by the sun's position below the horizon at specific angles (typically 15-18 degrees below, equivalent to zenith angles of 105-108 degrees). + +--- + +[Home](Home) . [API Reference](API-Reference) . [Architecture](Architecture) . [NREL SPA Algorithm](NREL-SPA-Algorithm) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f39bb9..6617946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,47 @@ # Changelog -All notable changes to this project will be documented in this file. +## [2.0.0] - 2026-02-25 + +### Added + +- TypeScript wrapper (`src/index.ts`, `src/types.ts`) with full type definitions +- Dual CJS and ESM builds via tsup (`dist/index.cjs`, `dist/index.mjs`) +- TypeScript declaration files (`dist/index.d.ts`, `dist/index.d.mts`) +- `formatTime()` utility export for converting fractional hours to `HH:MM:SS` +- Input validation with descriptive `TypeError` and `RangeError` messages +- Function code exports: `SPA_ZA`, `SPA_ZA_INC`, `SPA_ZA_RTS`, `SPA_ALL` +- Test suite: 61 ESM assertions and 17 CJS assertions +- GitHub Actions CI workflow (Node 20/22/24 matrix, typecheck, pack-check) +- GitHub Wiki with architecture, API reference, twilight calculations, and algorithm documentation +- NREL attribution in `LICENSE` and `README` +- `pnpm-workspace.yaml`, `.editorconfig`, `.npmrc`, `.nvmrc` config files + +### Changed + +- Core algorithm moved from `dist/spa.js` to `lib/spa.js` (same code, clearer location) +- `package.json` rewritten: proper `exports` map, `files`, `engines`, `sideEffects`, all required fields +- Author corrected to "Aric Camarata" +- `repository.url` corrected to use `git+https://` prefix (no npm publish warnings) +- `engines.node` set to `>=20` +- Description expanded with full keyword coverage +- LICENSE year corrected to `2023-2026` +- README rewritten with badges, full API tables, quick start, and NREL acknowledgments + +## [1.3.0] - 2025-05-04 + +- Major fix for discrepancies between this implementation and the original NREL C reference +- Added `bin/` folder for compiling and testing against the C reference executable +- All 10 global test cases now produce identical output to the C reference + +## [1.2.2] - 2023-11-12 + +- Moved timezone to main function arguments and changed default behavior +- Updated test cases and README + +## [1.1.0] - 2023-11-11 + +- Committed `dist/` folder (core algorithm) to git ## [1.0.0] - 2023-11-11 - Initial release - -## [1.1.0] - 2023-11-11 - -- Unignored "dist" folder in git (major) - -## [1.2.2] - 2023-11-12 - -- Moved timezone to main args and changed default behavior (major) -- Updated test cases and readme to reflect new usage (minor) - -## [1.3.0] - 2025-05-04 - -- Major update to fix discrepancies between original C and this implementation -- Folder "bin" added to compile and test against original C version -- This NPM now gives the exact same results as the original NREL-SPA - \ No newline at end of file diff --git a/LICENSE b/LICENSE index 11c9c33..284cbfc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Aric Camarata +Copyright (c) 2023-2026 Aric Camarata Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +19,47 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Third-Party Notice: NREL Solar Position Algorithm +-------------------------------------------------- + +The core algorithm in this package (lib/spa.js) is a JavaScript port of the +Solar Position Algorithm (SPA) developed at the National Renewable Energy +Laboratory (NREL) by Ibrahim Reda and Afshin Andreas. The original C source +files are subject to their own license terms, reproduced below: + + Copyright (C) 2008-2011 Alliance for Sustainable Energy, LLC, + All Rights Reserved + + The Solar Position Algorithm ("Software") is code in development prepared + by employees of the Alliance for Sustainable Energy, LLC, (hereinafter the + "Contractor"), under Contract No. DE-AC36-08GO28308 ("Contract") with the + U.S. Department of Energy (the "DOE"). The United States Government has + been granted for itself and others acting on its behalf a paid-up, non- + exclusive, irrevocable, worldwide license in the Software to reproduce, + prepare derivative works, and perform publicly and display publicly. + Beginning five (5) years after the date permission to assert copyright is + obtained from the DOE, and subject to any subsequent five (5) year + renewals, the United States Government is granted for itself and others + acting on its behalf a paid-up, non-exclusive, irrevocable, worldwide + license in the Software to reproduce, prepare derivative works, distribute + copies to the public, perform publicly and display publicly, and to permit + others to do so. If the Contractor ceases to make this computer software + available, it may be obtained from DOE's Office of Scientific and Technical + Information's Energy Science and Technology Software Center (ESTSC) at + P.O. Box 1020, Oak Ridge, TN 37831-1020. + + THIS SOFTWARE IS PROVIDED BY THE CONTRACTOR "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE CONTRACTOR OR THE U.S. GOVERNMENT BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER, + INCLUDING BUT NOT LIMITED TO CLAIMS ASSOCIATED WITH THE LOSS OF DATA OR + PROFITS, WHICH MAY RESULT FROM AN ACTION IN CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS CLAIM THAT ARISES OUT OF OR IN CONNECTION WITH THE ACCESS, USE OR + PERFORMANCE OF THIS SOFTWARE. + + Reference: Reda, I., Andreas, A. (2004). "Solar Position Algorithm for + Solar Radiation Applications." Solar Energy, 76(5), 577-589. + + Original source: https://midcdmz.nrel.gov/spa/ diff --git a/README.md b/README.md index 1987542..423bbc5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # nrel-spa -NREL SPA (Solar Position Algorithm) native implementation in JavaScript. This package allows for precise calculations of solar positions and phases based on geographical coordinates and time. +[![npm version](https://img.shields.io/npm/v/nrel-spa.svg)](https://www.npmjs.com/package/nrel-spa) +[![CI](https://github.com/acamarata/nrel-spa/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/nrel-spa/actions/workflows/ci.yml) +[![license](https://img.shields.io/npm/l/nrel-spa.svg)](./LICENSE) + +Pure JavaScript implementation of the NREL Solar Position Algorithm (SPA). Computes solar zenith angle, azimuth, sunrise, sunset, and solar noon for any location and date. Validated to produce identical results to the original NREL C reference implementation. ## Installation @@ -8,57 +12,165 @@ NREL SPA (Solar Position Algorithm) native implementation in JavaScript. This pa npm install nrel-spa ``` -## Usage - -Basic usage examples: +## Quick Start ```javascript -const { getSpa, calcSpa } = require('nrel-spa'); +import { getSpa, calcSpa } from 'nrel-spa'; -const date = new Date(); -console.log(date) +const date = new Date('2025-06-21T00:00:00Z'); // UTC date/time -// NYC - minimum params -const city = "New York" -const lat = 40.7128; -const lng = -74.006; -const tz = null // optional -const params = null // optional -const angles = [] // optional +// Minimum required parameters +const raw = getSpa(date, 40.7128, -74.006, -4); // New York, EDT (UTC-4) +console.log(raw.sunrise); // 5.417 (fractional hours) +console.log(raw.solarNoon); // 12.965 +console.log(raw.sunset); // 20.510 -/* Jakarta - all params -const city = "Jakarta" -const lat = -6.2088 -const lng = 106.8456 -const tz = 0 -const elevation = 18 -const temperature = 26.56 -const pressure = 1017 -const params = {elevation, temperature, pressure} -const angles = [63.435] */ +// Formatted output — same parameters, HH:MM:SS strings +const fmt = calcSpa(date, 40.7128, -74.006, -4); +console.log(fmt.sunrise); // "05:25:03" +console.log(fmt.solarNoon); // "12:57:56" +console.log(fmt.sunset); // "20:30:35" -// Get results -const get = getSpa(date, lat, lng); // minimum args -const calc = calcSpa(date, lat, lng, tz, params, angles); - -// Print results -console.log(`\nTest: ${city} with current Date():\n`) -console.log("getSpa =", get, "\n"); -console.log("calcSpa =", calc, "\n"); +// With atmospheric parameters and custom zenith angles (twilight) +const result = calcSpa( + date, + 40.7128, // latitude (degrees, negative = south) + -74.006, // longitude (degrees, negative = west) + -4, // timezone offset in hours from UTC + { + elevation: 10, // meters above sea level + pressure: 1013, // millibars + temperature: 20 // degrees Celsius + }, + [96, 102, 108], // civil, nautical, astronomical twilight zenith angles +); +console.log(result.sunrise); // "05:25:03" +console.log(result.angles[0]); // { sunrise: "04:53:...", sunset: "20:02:..." } ``` ## API -Exporting getSpa, calcSpa, and fractalTime. Only date, lat, lng are required but if timezone (tz) is null, will return in UTC. +### `getSpa(date, latitude, longitude, timezone?, options?, angles?)` -- getSpa(date, lat, lng, tz, params, angles) // returns SPA results in fractal times -- getSpa(date, lat, lng, tz, params, angles) // returns SPA results in formatted times -- fractalTime(fractionalHour) // formats time from 10.767407706732804 to 10:46:02.667 +Returns raw numerical values. Sunrise, solarNoon, and sunset are fractional hours (e.g., `5.417` for 05:25). -## Contributing +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `date` | `Date` | Yes | UTC date and time for the calculation | +| `latitude` | `number` | Yes | Observer latitude in degrees (-90 to 90) | +| `longitude` | `number` | Yes | Observer longitude in degrees (-180 to 180) | +| `timezone` | `number \| null` | No | Hours from UTC. Default: `0` | +| `options` | `SpaOptions \| null` | No | Atmospheric and calculation parameters | +| `angles` | `number[]` | No | Custom zenith angles in degrees for twilight | -Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md). +**Returns:** `SpaResult` (or `SpaResultWithAngles` when `angles` is provided) + +```typescript +interface SpaResult { + zenith: number; // topocentric zenith angle (degrees) + azimuth: number; // topocentric azimuth, eastward from north (degrees) + sunrise: number; // local sunrise time (fractional hours) + solarNoon: number; // local solar noon (fractional hours) + sunset: number; // local sunset time (fractional hours) +} +``` + +### `calcSpa(date, latitude, longitude, timezone?, options?, angles?)` + +Same as `getSpa()`, but formats sunrise, solarNoon, and sunset as `HH:MM:SS` strings. Returns `"N/A"` for those fields during polar day or polar night. + +### `formatTime(hours)` + +Converts fractional hours to `HH:MM:SS` format. Returns `"N/A"` for negative or non-finite values. + +```javascript +import { formatTime } from 'nrel-spa'; +formatTime(5.417489); // "05:25:03" +formatTime(-1); // "N/A" +``` + +### `SpaOptions` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `elevation` | `number` | `0` | Observer elevation in meters | +| `pressure` | `number` | `1013` | Atmospheric pressure in millibars | +| `temperature` | `number` | `15` | Temperature in degrees Celsius | +| `delta_ut1` | `number` | `0` | UT1-UTC correction in seconds | +| `delta_t` | `number` | `67` | TT-UTC difference in seconds | +| `slope` | `number` | `0` | Surface slope from horizontal (degrees) | +| `azm_rotation` | `number` | `0` | Surface azimuth rotation from south (degrees) | +| `atmos_refract` | `number` | `0.5667` | Atmospheric refraction at sunrise/sunset (degrees) | +| `function` | `SpaFunctionCode` | `SPA_ZA_RTS` | Which outputs to compute | + +### Function Codes + +```javascript +import { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from 'nrel-spa'; +``` + +| Code | Value | Computes | +| --- | --- | --- | +| `SPA_ZA` | `0` | Zenith and azimuth only | +| `SPA_ZA_INC` | `1` | Zenith, azimuth, and incidence angle | +| `SPA_ZA_RTS` | `2` | Zenith, azimuth, sunrise, noon, sunset (default) | +| `SPA_ALL` | `3` | All outputs | + +## Architecture + +The core algorithm in `lib/spa.js` is a direct port of the NREL SPA C source to JavaScript, preserving the same mathematical structure: 63 periodic nutation terms, full heliocentric coordinate calculation, topocentric correction, and atmospheric refraction. It has been validated to produce output identical to the C reference within rounding. + +The TypeScript wrapper in `src/` provides the public API, input validation, and the `formatTime` utility. The package ships dual CJS and ESM builds via tsup, with full TypeScript definitions. + +See the [wiki](https://github.com/acamarata/nrel-spa/wiki) for a detailed breakdown of the algorithm and architecture. + +## Compatibility + +- **Node.js:** 20, 22, 24 (CI tested) +- **ESM:** `import { getSpa } from 'nrel-spa'` +- **CommonJS:** `const { getSpa } = require('nrel-spa')` +- **TypeScript:** Full type definitions included + +Bundlers (Vite, Webpack, esbuild, Rollup) work via the `exports` map in package.json. + +## TypeScript + +```typescript +import { + getSpa, + calcSpa, + formatTime, + SPA_ZA_RTS, + type SpaResult, + type SpaFormattedResult, + type SpaOptions, +} from 'nrel-spa'; +``` + +## Documentation + +Full documentation is available on the [GitHub Wiki](https://github.com/acamarata/nrel-spa/wiki): + +- [API Reference](https://github.com/acamarata/nrel-spa/wiki/API-Reference) +- [Architecture](https://github.com/acamarata/nrel-spa/wiki/Architecture) +- [Twilight Calculations](https://github.com/acamarata/nrel-spa/wiki/Twilight-Calculations) +- [NREL SPA Algorithm](https://github.com/acamarata/nrel-spa/wiki/NREL-SPA-Algorithm) + +## Related + +Other packages in this collection: + +- [solar-spa](https://www.npmjs.com/package/solar-spa): WASM build of the same algorithm, async, for high-throughput batch calculations +- [pray-calc](https://www.npmjs.com/package/pray-calc): Islamic prayer times built on nrel-spa + +## Acknowledgments + +The core algorithm is a JavaScript port of the Solar Position Algorithm (SPA) developed by Ibrahim Reda and Afshin Andreas at the National Renewable Energy Laboratory (NREL): + +> Reda, I., Andreas, A. (2004). "Solar Position Algorithm for Solar Radiation Applications." Solar Energy, 76(5), 577-589. [https://doi.org/10.1016/j.solener.2003.12.003](https://doi.org/10.1016/j.solener.2003.12.003) + +Original source: [https://midcdmz.nrel.gov/spa/](https://midcdmz.nrel.gov/spa/) ## License -This project is licensed under the ISC License - see the [LICENSE](LICENSE) file for details. +MIT (TypeScript wrapper and build tooling). The core algorithm in `lib/spa.js` is a port of NREL's SPA C source, which is subject to its own terms. See the [LICENSE](./LICENSE) file for the full notice. diff --git a/index.js b/index.js deleted file mode 100644 index 499fb35..0000000 --- a/index.js +++ /dev/null @@ -1,118 +0,0 @@ -// index.js -const spa = require('./dist/spa'); - -/** - * Convert fractional hours to HH:MM:SS.mmm (rounding total seconds) - */ -function fractalTime(fractionalHour) { - const totalSec = Math.round(fractionalHour * 3600); - const H = Math.floor(totalSec / 3600); - const rem = totalSec - H * 3600; - const M = Math.floor(rem / 60); - const S = rem - M * 60; - const ms = Math.round((fractionalHour * 3600 - Math.floor(fractionalHour * 3600)) * 1000); - return `${H.toString().padStart(2,'0')}:` + - `${M.toString().padStart(2,'0')}:` + - `${S.toString().padStart(2,'0')}.` + - `${ms.toString().padStart(3,'0')}`; -} - -/** - * Re-solve hour-angle for a custom zenith angle Zdeg (in degrees) - */ -function adjustForCustomAngle(base, Zdeg) { - const φ = base.latitude * Math.PI/180; - const δ = base.delta * Math.PI/180; - const Z = Zdeg * Math.PI/180; - const cosH0 = (Math.cos(Z) - Math.sin(φ) * Math.sin(δ)) / - (Math.cos(φ) * Math.cos(δ)); - if (cosH0 < -1 || cosH0 > 1) { - return { ...base, sunrise: NaN, sunset: NaN }; - } - const H0h = (Math.acos(cosH0) * 180/Math.PI) / 15; - return { - ...base, - sunrise: base.suntransit - H0h, - sunset: base.suntransit + H0h - }; -} - -/** - * Core SPA data calculation (raw fractional hours) - * @param {Date} date - JavaScript Date (UTC) - * @param {number} lat - * @param {number} lng - * @param {number} tz - timezone offset in hours (e.g. -4 for EDT) - * @param {object} params - { elevation, pressure, temperature, delta_ut1, delta_t, slope, azm_rotation, atmos_refract } - * @param {number[]} angles - custom zenith angles (deg) for twilight - */ -function getSpa(date, lat, lng, tz = 0, params = {}, angles = []) { - const d = new spa.SpaData(); - // Use UTC components and explicit tz - d.year = date.getUTCFullYear(); - d.month = date.getUTCMonth() + 1; - d.day = date.getUTCDate(); - d.hour = date.getUTCHours(); - d.minute = date.getUTCMinutes(); - d.second = date.getUTCSeconds(); - d.longitude = lng; - d.latitude = lat; - d.timezone = tz; - - // Align defaults to reference C code - d.elevation = params.elevation ?? 0; - d.pressure = params.pressure ?? 1013; - d.temperature = params.temperature ?? 15; - d.delta_ut1 = params.delta_ut1 ?? 0; - d.delta_t = params.delta_t ?? 67; - d.slope = params.slope ?? 0; - d.azm_rotation = params.azm_rotation ?? 0; - d.atmos_refract= params.atmos_refract?? 0.5667; - - // Only compute ZA and rise/transit/set - d.function = spa.SPA_ZA_RTS; - - const rc = spa.spa_calculate(d); - if (rc !== 0) { - throw new Error(`SPA calculation failed with code ${rc}`); - } - - // Base outputs - const output = { - zenith: d.zenith, - azimuth: d.azimuth, - sunrise: d.sunrise, - solarNoon: d.suntransit, - sunset: d.sunset - }; - - // Custom angles (twilight) - if (angles.length) { - output.angles = angles.map(Z => { - const c = adjustForCustomAngle(d, Z); - return { sunrise: c.sunrise, sunset: c.sunset }; - }); - } - - return output; -} - -/** - * Same as getSpa, but formats sunrise/noon/sunset to strings - */ -function calcSpa(date, lat, lng, tz = 0, params = {}, angles = []) { - const raw = getSpa(date, lat, lng, tz, params, angles); - return { - zenith: raw.zenith, - azimuth: raw.azimuth, - sunrise: fractalTime(raw.sunrise), - solarNoon: fractalTime(raw.solarNoon), - sunset: fractalTime(raw.sunset), - angles: raw.angles ? raw.angles.map(a => ({ - sunrise: fractalTime(a.sunrise), - sunset: fractalTime(a.sunset) - })) : undefined - }; -} - -module.exports = { getSpa, calcSpa, fractalTime, adjustForCustomAngle }; \ No newline at end of file diff --git a/lib/spa.js b/lib/spa.js new file mode 100644 index 0000000..787ca16 --- /dev/null +++ b/lib/spa.js @@ -0,0 +1,989 @@ +// dist/spa.js +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.spa_calculate = exports.topocentric_azimuth_angle = exports.topocentric_azimuth_angle_astro = exports.topocentric_zenith_angle = exports.topocentric_elevation_angle_corrected = exports.atmospheric_refraction_correction = exports.topocentric_elevation_angle = exports.topocentric_local_hour_angle = exports.topocentric_right_ascension = exports.right_ascension_parallax_and_topocentric_dec = exports.observer_hour_angle = exports.geocentric_declination = exports.geocentric_right_ascension = exports.third_order_polynomial = exports.limit_degrees = exports.rad2deg = exports.deg2rad = exports.SpaData = exports.SPA_ALL = exports.SPA_ZA_RTS = exports.SPA_ZA_INC = exports.SPA_ZA = exports.OutCode = void 0; +var OutCode; +(function (OutCode) { + OutCode[OutCode["SPA_ZA"] = 0] = "SPA_ZA"; + OutCode[OutCode["SPA_ZA_INC"] = 1] = "SPA_ZA_INC"; + OutCode[OutCode["SPA_ZA_RTS"] = 2] = "SPA_ZA_RTS"; + OutCode[OutCode["SPA_ALL"] = 3] = "SPA_ALL"; //calculate all SPA output values +})(OutCode = exports.OutCode || (exports.OutCode = {})); +//For external reference as per original C definition +exports.SPA_ZA = OutCode.SPA_ZA; +exports.SPA_ZA_INC = OutCode.SPA_ZA_INC; +exports.SPA_ZA_RTS = OutCode.SPA_ZA_RTS; +exports.SPA_ALL = OutCode.SPA_ALL; +var PI = Math.PI; +var SUN_RADIUS = 0.26667; +var L_COUNT = 6; +var B_COUNT = 2; +var R_COUNT = 5; +var Y_COUNT = 63; +var TermsA; +(function (TermsA) { + TermsA[TermsA["TERM_A"] = 0] = "TERM_A"; + TermsA[TermsA["TERM_B"] = 1] = "TERM_B"; + TermsA[TermsA["TERM_C"] = 2] = "TERM_C"; + TermsA[TermsA["TERM_COUNT"] = 3] = "TERM_COUNT"; +})(TermsA || (TermsA = {})); +var TermsX; +(function (TermsX) { + TermsX[TermsX["TERM_X0"] = 0] = "TERM_X0"; + TermsX[TermsX["TERM_X1"] = 1] = "TERM_X1"; + TermsX[TermsX["TERM_X2"] = 2] = "TERM_X2"; + TermsX[TermsX["TERM_X3"] = 3] = "TERM_X3"; + TermsX[TermsX["TERM_X4"] = 4] = "TERM_X4"; + TermsX[TermsX["TERM_X_COUNT"] = 5] = "TERM_X_COUNT"; +})(TermsX || (TermsX = {})); +var TermsPE; +(function (TermsPE) { + TermsPE[TermsPE["TERM_PSI_A"] = 0] = "TERM_PSI_A"; + TermsPE[TermsPE["TERM_PSI_B"] = 1] = "TERM_PSI_B"; + TermsPE[TermsPE["TERM_EPS_C"] = 2] = "TERM_EPS_C"; + TermsPE[TermsPE["TERM_EPS_D"] = 3] = "TERM_EPS_D"; + TermsPE[TermsPE["TERM_PE_COUNT"] = 4] = "TERM_PE_COUNT"; +})(TermsPE || (TermsPE = {})); +var JDSign; +(function (JDSign) { + JDSign[JDSign["JD_MINUS"] = 0] = "JD_MINUS"; + JDSign[JDSign["JD_ZERO"] = 1] = "JD_ZERO"; + JDSign[JDSign["JD_PLUS"] = 2] = "JD_PLUS"; + JDSign[JDSign["JD_COUNT"] = 3] = "JD_COUNT"; +})(JDSign || (JDSign = {})); +var SunState; +(function (SunState) { + SunState[SunState["SUN_TRANSIT"] = 0] = "SUN_TRANSIT"; + SunState[SunState["SUN_RISE"] = 1] = "SUN_RISE"; + SunState[SunState["SUN_SET"] = 2] = "SUN_SET"; + SunState[SunState["SUN_COUNT"] = 3] = "SUN_COUNT"; +})(SunState || (SunState = {})); +var TERM_Y_COUNT = TermsX.TERM_X_COUNT; +var l_subcount = [64, 34, 20, 7, 3, 1]; +var b_subcount = [5, 2]; +var r_subcount = [40, 10, 6, 2, 1]; +var SpaData = /** @class */ (function () { + function SpaData() { + //--------------------------Input Valuse + this.year = 0; // 4-digit year, valid range: -2000 to 6000, error code: 1 + this.month = 0; // 2-digit month, valid range: 1 to 12, error code: 2 + this.day = 0; // 2-digit day, valid range: 1 to 31, error code: 3 + this.hour = 0; // Observer local hour, valid range: 0 to 24, error code: 4 + this.minute = 0; // Observer local minute, valid range: 0 to 59, error code: 5 + this.second = 0.0; // Observer local second, valid range: 0 to <60, error code: 6 + this.delta_ut1 = 0.0; // Fractional second difference between UTC and UT which is used + // to adjust UTC for earth's irregular rotation rate and is derived + // from observation only and is reported in this bulletin: + // http://maia.usno.navy.mil/ser7/ser7.dat, + // where delta_ut1 = DUT1 + // valid range: -1 to 1 second (exclusive), error code 17 + this.delta_t = 0.0; // Difference between earth rotation time and terrestrial time + // It is derived from observation only and is reported in this + // bulletin: http://maia.usno.navy.mil/ser7/ser7.dat, + // where delta_t = 32.184 + (TAI-UTC) - DUT1 + // valid range: -8000 to 8000 seconds, error code: 7 + this.timezone = 0.0; // Observer time zone (negative west of Greenwich) + // valid range: -18 to 18 hours, error code: 8 + this.longitude = 0.0; // Observer longitude (negative west of Greenwich) + // valid range: -180 to 180 degrees, error code: 9 + this.latitude = 0.0; // Observer latitude (negative south of equator) + // valid range: -90 to 90 degrees, error code: 10 + this.elevation = 0.0; // Observer elevation [meters] + // valid range: -6500000 or higher meters, error code: 11 + this.pressure = 0.0; // Annual average local pressure [millibars] + // valid range: 0 to 5000 millibars, error code: 12 + this.temperature = 0.0; // Annual average local temperature [degrees Celsius] + // valid range: -273 to 6000 degrees Celsius, error code 13 + this.slope = 0.0; // Surface slope (measured from the horizontal plane) + // valid range: -360 to 360 degrees, error code: 14 + this.azm_rotation = 0.0; // Surface azimuth rotation (measured from south to projection of + // surface normal on horizontal plane, negative east) + // valid range: -360 to 360 degrees, error code: 15 + this.atmos_refract = 0.0; // Atmospheric refraction at sunrise and sunset (0.5667 deg is typical) + // valid range: -5 to 5 degrees, error code: 16 + this.function = 0; // Switch to choose functions for desired output (from enumeration) + //-----------------ermediate OUTPUT VALUES-------------------- + this.jd = 0.0; //Julian day + this.jc = 0.0; //Julian century + this.jde = 0.0; //Julian ephemeris day + this.jce = 0.0; //Julian ephemeris century + this.jme = 0.0; //Julian ephemeris millennium + this.l = 0.0; //earth heliocentric longitude [degrees] + this.b = 0.0; //earth heliocentric latitude [degrees] + this.r = 0.0; //earth radius vector [Astronomical Units, AU] + this.theta = 0.0; //geocentric longitude [degrees] + this.beta = 0.0; //geocentric latitude [degrees] + this.x0 = 0.0; //mean elongation (moon-sun) [degrees] + this.x1 = 0.0; //mean anomaly (sun) [degrees] + this.x2 = 0.0; //mean anomaly (moon) [degrees] + this.x3 = 0.0; //argument latitude (moon) [degrees] + this.x4 = 0.0; //ascending longitude (moon) [degrees] + this.del_psi = 0.0; //nutation longitude [degrees] + this.del_epsilon = 0.0; //nutation obliquity [degrees] + this.epsilon0 = 0.0; //ecliptic mean obliquity [arc seconds] + this.epsilon = 0.0; //ecliptic true obliquity [degrees] + this.del_tau = 0.0; //aberration correction [degrees] + this.lamda = 0.0; //apparent sun longitude [degrees] + this.nu0 = 0.0; //Greenwich mean sidereal time [degrees] + this.nu = 0.0; //Greenwich sidereal time [degrees] + this.alpha = 0.0; //geocentric sun right ascension [degrees] + this.delta = 0.0; //geocentric sun declination [degrees] + this.h = 0.0; //observer hour angle [degrees] + this.xi = 0.0; //sun equatorial horizontal parallax [degrees] + this.del_alpha = 0.0; //sun right ascension parallax [degrees] + this.delta_prime = 0.0; //topocentric sun declination [degrees] + this.alpha_prime = 0.0; //topocentric sun right ascension [degrees] + this.h_prime = 0.0; //topocentric local hour angle [degrees] + this.e0 = 0.0; //topocentric elevation angle (uncorrected) [degrees] + this.del_e = 0.0; //atmospheric refraction correction [degrees] + this.e = 0.0; //topocentric elevation angle (corrected) [degrees] + this.eot = 0.0; //equation of time [minutes] + this.srha = 0.0; //sunrise hour angle [degrees] + this.ssha = 0.0; //sunset hour angle [degrees] + this.sta = 0.0; //sun transit altitude [degrees] + //---------------------Final OUTPUT VALUES------------------------ + this.zenith = 0.0; //topocentric zenith angle [degrees] + this.azimuth_astro = 0.0; //topocentric azimuth angle (westward from south) [for astronomers] + this.azimuth = 0.0; //topocentric azimuth angle (eastward from north) [for navigators and solar radiation] + this.incidence = 0.0; //surface incidence angle [degrees] + this.suntransit = 0.0; //local sun transit time (or solar noon) [fractional hour] + this.sunrise = 0.0; //local sunrise time (+/- 30 seconds) [fractional hour] + this.sunset = 0.0; //local sunset time (+/- 30 seconds) [fractional hour] + } + return SpaData; +}()); +exports.SpaData = SpaData; +var copySPA = function (src) { + return Array.isArray(src) + ? src.map(function (i) { return copySPA(i); }) + : typeof src === 'object' + ? Object.getOwnPropertyNames(src).reduce(function (o, prop) { + Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(src, prop)); + o[prop] = copySPA(src[prop]); + return o; + }, Object.create(Object.getPrototypeOf(src))) + : src; +}; +//===================Earth Periodic Terms=================== +var L_TERMS = [ + [ + [175347046.0, 0, 0], + [3341656.0, 4.6692568, 6283.07585], + [34894.0, 4.6261, 12566.1517], + [3497.0, 2.7441, 5753.3849], + [3418.0, 2.8289, 3.5231], + [3136.0, 3.6277, 77713.7715], + [2676.0, 4.4181, 7860.4194], + [2343.0, 6.1352, 3930.2097], + [1324.0, 0.7425, 11506.7698], + [1273.0, 2.0371, 529.691], + [1199.0, 1.1096, 1577.3435], + [990, 5.233, 5884.927], + [902, 2.045, 26.298], + [857, 3.508, 398.149], + [780, 1.179, 5223.694], + [753, 2.533, 5507.553], + [505, 4.583, 18849.228], + [492, 4.205, 775.523], + [357, 2.92, 0.067], + [317, 5.849, 11790.629], + [284, 1.899, 796.298], + [271, 0.315, 10977.079], + [243, 0.345, 5486.778], + [206, 4.806, 2544.314], + [205, 1.869, 5573.143], + [202, 2.458, 6069.777], + [156, 0.833, 213.299], + [132, 3.411, 2942.463], + [126, 1.083, 20.775], + [115, 0.645, 0.98], + [103, 0.636, 4694.003], + [102, 0.976, 15720.839], + [102, 4.267, 7.114], + [99, 6.21, 2146.17], + [98, 0.68, 155.42], + [86, 5.98, 161000.69], + [85, 1.3, 6275.96], + [85, 3.67, 71430.7], + [80, 1.81, 17260.15], + [79, 3.04, 12036.46], + [75, 1.76, 5088.63], + [74, 3.5, 3154.69], + [74, 4.68, 801.82], + [70, 0.83, 9437.76], + [62, 3.98, 8827.39], + [61, 1.82, 7084.9], + [57, 2.78, 6286.6], + [56, 4.39, 14143.5], + [56, 3.47, 6279.55], + [52, 0.19, 12139.55], + [52, 1.33, 1748.02], + [51, 0.28, 5856.48], + [49, 0.49, 1194.45], + [41, 5.37, 8429.24], + [41, 2.4, 19651.05], + [39, 6.17, 10447.39], + [37, 6.04, 10213.29], + [37, 2.57, 1059.38], + [36, 1.71, 2352.87], + [36, 1.78, 6812.77], + [33, 0.59, 17789.85], + [30, 0.44, 83996.85], + [30, 2.74, 1349.87], + [25, 3.16, 4690.48] + ], + [ + [628331966747.0, 0, 0], + [206059.0, 2.678235, 6283.07585], + [4303.0, 2.6351, 12566.1517], + [425.0, 1.59, 3.523], + [119.0, 5.796, 26.298], + [109.0, 2.966, 1577.344], + [93, 2.59, 18849.23], + [72, 1.14, 529.69], + [68, 1.87, 398.15], + [67, 4.41, 5507.55], + [59, 2.89, 5223.69], + [56, 2.17, 155.42], + [45, 0.4, 796.3], + [36, 0.47, 775.52], + [29, 2.65, 7.11], + [21, 5.34, 0.98], + [19, 1.85, 5486.78], + [19, 4.97, 213.3], + [17, 2.99, 6275.96], + [16, 0.03, 2544.31], + [16, 1.43, 2146.17], + [15, 1.21, 10977.08], + [12, 2.83, 1748.02], + [12, 3.26, 5088.63], + [12, 5.27, 1194.45], + [12, 2.08, 4694], + [11, 0.77, 553.57], + [10, 1.3, 6286.6], + [10, 4.24, 1349.87], + [9, 2.7, 242.73], + [9, 5.64, 951.72], + [8, 5.3, 2352.87], + [6, 2.65, 9437.76], + [6, 4.67, 4690.48] + ], + [ + [52919.0, 0, 0], + [8720.0, 1.0721, 6283.0758], + [309.0, 0.867, 12566.152], + [27, 0.05, 3.52], + [16, 5.19, 26.3], + [16, 3.68, 155.42], + [10, 0.76, 18849.23], + [9, 2.06, 77713.77], + [7, 0.83, 775.52], + [5, 4.66, 1577.34], + [4, 1.03, 7.11], + [4, 3.44, 5573.14], + [3, 5.14, 796.3], + [3, 6.05, 5507.55], + [3, 1.19, 242.73], + [3, 6.12, 529.69], + [3, 0.31, 398.15], + [3, 2.28, 553.57], + [2, 4.38, 5223.69], + [2, 3.75, 0.98] + ], + [ + [289.0, 5.844, 6283.076], + [35, 0, 0], + [17, 5.49, 12566.15], + [3, 5.2, 155.42], + [1, 4.72, 3.52], + [1, 5.3, 18849.23], + [1, 5.97, 242.73] + ], + [ + [114.0, 3.142, 0], + [8, 4.13, 6283.08], + [1, 3.84, 12566.15] + ], + [ + [1, 3.14, 0] + ] +]; +var B_TERMS = [ + [ + [280.0, 3.199, 84334.662], + [102.0, 5.422, 5507.553], + [80, 3.88, 5223.69], + [44, 3.7, 2352.87], + [32, 4, 1577.34] + ], + [ + [9, 3.9, 5507.55], + [6, 1.73, 5223.69] + ] +]; +var R_TERMS = [ + [ + [100013989.0, 0, 0], + [1670700.0, 3.0984635, 6283.07585], + [13956.0, 3.05525, 12566.1517], + [3084.0, 5.1985, 77713.7715], + [1628.0, 1.1739, 5753.3849], + [1576.0, 2.8469, 7860.4194], + [925.0, 5.453, 11506.77], + [542.0, 4.564, 3930.21], + [472.0, 3.661, 5884.927], + [346.0, 0.964, 5507.553], + [329.0, 5.9, 5223.694], + [307.0, 0.299, 5573.143], + [243.0, 4.273, 11790.629], + [212.0, 5.847, 1577.344], + [186.0, 5.022, 10977.079], + [175.0, 3.012, 18849.228], + [110.0, 5.055, 5486.778], + [98, 0.89, 6069.78], + [86, 5.69, 15720.84], + [86, 1.27, 161000.69], + [65, 0.27, 17260.15], + [63, 0.92, 529.69], + [57, 2.01, 83996.85], + [56, 5.24, 71430.7], + [49, 3.25, 2544.31], + [47, 2.58, 775.52], + [45, 5.54, 9437.76], + [43, 6.01, 6275.96], + [39, 5.36, 4694], + [38, 2.39, 8827.39], + [37, 0.83, 19651.05], + [37, 4.9, 12139.55], + [36, 1.67, 12036.46], + [35, 1.84, 2942.46], + [33, 0.24, 7084.9], + [32, 0.18, 5088.63], + [32, 1.78, 398.15], + [28, 1.21, 6286.6], + [28, 1.9, 6279.55], + [26, 4.59, 10447.39] + ], + [ + [103019.0, 1.10749, 6283.07585], + [1721.0, 1.0644, 12566.1517], + [702.0, 3.142, 0], + [32, 1.02, 18849.23], + [31, 2.84, 5507.55], + [25, 1.32, 5223.69], + [18, 1.42, 1577.34], + [10, 5.91, 10977.08], + [9, 1.42, 6275.96], + [9, 0.27, 5486.78] + ], + [ + [4359.0, 5.7846, 6283.0758], + [124.0, 5.579, 12566.152], + [12, 3.14, 0], + [9, 3.63, 77713.77], + [6, 1.87, 5573.14], + [3, 5.47, 18849.23] + ], + [ + [145.0, 4.273, 6283.076], + [7, 3.92, 12566.15] + ], + [ + [4, 2.56, 6283.08] + ] +]; +//===================Periodic Terms for the nutation in longitude and obliquity=================== +var Y_TERMS = [ + [0, 0, 0, 0, 1], + [-2, 0, 0, 2, 2], + [0, 0, 0, 2, 2], + [0, 0, 0, 0, 2], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [-2, 1, 0, 2, 2], + [0, 0, 0, 2, 1], + [0, 0, 1, 2, 2], + [-2, -1, 0, 2, 2], + [-2, 0, 1, 0, 0], + [-2, 0, 0, 2, 1], + [0, 0, -1, 2, 2], + [2, 0, 0, 0, 0], + [0, 0, 1, 0, 1], + [2, 0, -1, 2, 2], + [0, 0, -1, 0, 1], + [0, 0, 1, 2, 1], + [-2, 0, 2, 0, 0], + [0, 0, -2, 2, 1], + [2, 0, 0, 2, 2], + [0, 0, 2, 2, 2], + [0, 0, 2, 0, 0], + [-2, 0, 1, 2, 2], + [0, 0, 0, 2, 0], + [-2, 0, 0, 2, 0], + [0, 0, -1, 2, 1], + [0, 2, 0, 0, 0], + [2, 0, -1, 0, 1], + [-2, 2, 0, 2, 2], + [0, 1, 0, 0, 1], + [-2, 0, 1, 0, 1], + [0, -1, 0, 0, 1], + [0, 0, 2, -2, 0], + [2, 0, -1, 2, 1], + [2, 0, 1, 2, 2], + [0, 1, 0, 2, 2], + [-2, 1, 1, 0, 0], + [0, -1, 0, 2, 2], + [2, 0, 0, 2, 1], + [2, 0, 1, 0, 0], + [-2, 0, 2, 2, 2], + [-2, 0, 1, 2, 1], + [2, 0, -2, 0, 1], + [2, 0, 0, 0, 1], + [0, -1, 1, 0, 0], + [-2, -1, 0, 2, 1], + [-2, 0, 0, 0, 1], + [0, 0, 2, 2, 1], + [-2, 0, 2, 0, 1], + [-2, 1, 0, 2, 1], + [0, 0, 1, -2, 0], + [-1, 0, 1, 0, 0], + [-2, 1, 0, 0, 0], + [1, 0, 0, 0, 0], + [0, 0, 1, 2, 0], + [0, 0, -2, 2, 2], + [-1, -1, 1, 0, 0], + [0, 1, 1, 0, 0], + [0, -1, 1, 2, 2], + [2, -1, -1, 2, 2], + [0, 0, 3, 2, 2], + [2, -1, 0, 2, 2], +]; +var PE_TERMS = [ + [-171996, -174.2, 92025, 8.9], + [-13187, -1.6, 5736, -3.1], + [-2274, -0.2, 977, -0.5], + [2062, 0.2, -895, 0.5], + [1426, -3.4, 54, -0.1], + [712, 0.1, -7, 0], + [-517, 1.2, 224, -0.6], + [-386, -0.4, 200, 0], + [-301, 0, 129, -0.1], + [217, -0.5, -95, 0.3], + [-158, 0, 0, 0], + [129, 0.1, -70, 0], + [123, 0, -53, 0], + [63, 0, 0, 0], + [63, 0.1, -33, 0], + [-59, 0, 26, 0], + [-58, -0.1, 32, 0], + [-51, 0, 27, 0], + [48, 0, 0, 0], + [46, 0, -24, 0], + [-38, 0, 16, 0], + [-31, 0, 13, 0], + [29, 0, 0, 0], + [29, 0, -12, 0], + [26, 0, 0, 0], + [-22, 0, 0, 0], + [21, 0, -10, 0], + [17, -0.1, 0, 0], + [16, 0, -8, 0], + [-16, 0.1, 7, 0], + [-15, 0, 9, 0], + [-13, 0, 7, 0], + [-12, 0, 6, 0], + [11, 0, 0, 0], + [-10, 0, 5, 0], + [-8, 0, 3, 0], + [7, 0, -3, 0], + [-7, 0, 0, 0], + [-7, 0, 3, 0], + [-7, 0, 3, 0], + [6, 0, 0, 0], + [6, 0, -3, 0], + [6, 0, -3, 0], + [-6, 0, 3, 0], + [-6, 0, 3, 0], + [5, 0, 0, 0], + [-5, 0, 3, 0], + [-5, 0, 3, 0], + [-5, 0, 3, 0], + [4, 0, 0, 0], + [4, 0, 0, 0], + [4, 0, 0, 0], + [-4, 0, 0, 0], + [-4, 0, 0, 0], + [-4, 0, 0, 0], + [3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], +]; +//=================== Utility functions for other applications (such as NREL's SAMPA) -------------- +function deg2rad(degrees) { + return (PI / 180.0) * degrees; +} +exports.deg2rad = deg2rad; +function rad2deg(radians) { + return (180.0 / PI) * radians; +} +exports.rad2deg = rad2deg; +function limit_degrees(degrees) { + degrees /= 360; + var limited = 360 * (degrees - Math.floor(degrees)); + if (limited < 0) { + limited += 360; + } + return limited; +} +exports.limit_degrees = limit_degrees; +function third_order_polynomial(a, b, c, d, x) { + return ((a * x + b) + c) * x + d; +} +exports.third_order_polynomial = third_order_polynomial; +function geocentric_right_ascension(lamda, epsilon, beta) { + var lambdaRad = deg2rad(lamda); + var epsilonRad = deg2rad(epsilon); + return limit_degrees(rad2deg(Math.atan2(Math.sin(lambdaRad) * Math.cos(epsilonRad) - + Math.tan(deg2rad(beta)) * Math.sin(epsilonRad), Math.cos(lambdaRad)))); +} +exports.geocentric_right_ascension = geocentric_right_ascension; +function geocentric_declination(beta, epsilon, lamda) { + var betaRad = deg2rad(beta); + var epsilonRad = deg2rad(epsilon); + return rad2deg(Math.asin(Math.sin(betaRad) * Math.cos(epsilonRad) + + Math.cos(betaRad) * Math.sin(epsilonRad) * Math.sin(deg2rad(lamda)))); +} +exports.geocentric_declination = geocentric_declination; +function observer_hour_angle(nu, longitude, alpha_deg) { + return limit_degrees(nu + longitude - alpha_deg); +} +exports.observer_hour_angle = observer_hour_angle; +function right_ascension_parallax_and_topocentric_dec(latitude, elevation, xi, h, delta, dltap) { + var delta_alpha_rad = 0; + var lat_rad = deg2rad(latitude); + var xi_rad = deg2rad(xi); + var h_rad = deg2rad(h); + var delta_rad = deg2rad(delta); + var u = Math.atan(0.99664719 * Math.tan(lat_rad)); + var y = 0.99664719 * Math.sin(u) + elevation * Math.sin(lat_rad) / 6378140.0; + var x = Math.cos(u) + elevation * Math.cos(lat_rad) / 6378140.0; + delta_alpha_rad = Math.atan2(-x * Math.sin(xi_rad) * Math.sin(h_rad), Math.cos(delta_rad) - x * Math.sin(xi_rad) * Math.cos(h_rad)); + dltap.delta_prime = rad2deg(Math.atan2((Math.sin(delta_rad) - y * Math.sin(xi_rad)) * Math.cos(delta_alpha_rad), Math.cos(delta_rad) - x * Math.sin(xi_rad) * Math.cos(h_rad))); + dltap.delta_alpha = rad2deg(delta_alpha_rad); +} +exports.right_ascension_parallax_and_topocentric_dec = right_ascension_parallax_and_topocentric_dec; +function topocentric_right_ascension(alpha_deg, delta_alpha) { + return alpha_deg + delta_alpha; +} +exports.topocentric_right_ascension = topocentric_right_ascension; +function topocentric_local_hour_angle(h, delta_alpha) { + return h - delta_alpha; +} +exports.topocentric_local_hour_angle = topocentric_local_hour_angle; +function topocentric_elevation_angle(latitude, delta_prime, h_prime) { + var latRad = deg2rad(latitude); + var deltaPrimeRad = deg2rad(delta_prime); + return rad2deg(Math.asin(Math.sin(latRad) * Math.sin(deltaPrimeRad) + + Math.cos(latRad) * Math.cos(deltaPrimeRad) * Math.cos(deg2rad(h_prime)))); +} +exports.topocentric_elevation_angle = topocentric_elevation_angle; +function atmospheric_refraction_correction(pressure, temperature, atmos_refract, e0) { + var delE = 0; + if (e0 >= -1 * (SUN_RADIUS + atmos_refract)) + delE = (pressure / 1010.0) * (283.0 / (273.0 + temperature)) * + 1.02 / (60.0 * Math.tan(deg2rad(e0 + 10.3 / (e0 + 5.11)))); + return delE; +} +exports.atmospheric_refraction_correction = atmospheric_refraction_correction; +function topocentric_elevation_angle_corrected(e0, delta_e) { + return e0 + delta_e; +} +exports.topocentric_elevation_angle_corrected = topocentric_elevation_angle_corrected; +function topocentric_zenith_angle(e) { + return 90.0 - e; +} +exports.topocentric_zenith_angle = topocentric_zenith_angle; +function topocentric_azimuth_angle_astro(h_prime, latitude, delta_prime) { + var h_prime_rad = deg2rad(h_prime); + var lat_rad = deg2rad(latitude); + return limit_degrees(rad2deg(Math.atan2(Math.sin(h_prime_rad), Math.cos(h_prime_rad) * Math.sin(lat_rad) - Math.tan(deg2rad(delta_prime)) * Math.cos(lat_rad)))); +} +exports.topocentric_azimuth_angle_astro = topocentric_azimuth_angle_astro; +function topocentric_azimuth_angle(azimuth_astro) { + return limit_degrees(azimuth_astro + 180.0); +} +exports.topocentric_azimuth_angle = topocentric_azimuth_angle; +//=================== Local Utility functions =================== +function integer(val) { + return Math.floor(val); +} +//=============================================================== +function validate_inputs(spa) { + if ((spa.year < -2000) || (spa.year > 6000)) + return 1; + if ((spa.month < 1) || (spa.month > 12)) + return 2; + if ((spa.day < 1) || (spa.day > 31)) + return 3; + if ((spa.hour < 0) || (spa.hour > 24)) + return 4; + if ((spa.minute < 0) || (spa.minute > 59)) + return 5; + if ((spa.second < 0) || (spa.second >= 60)) + return 6; + if ((spa.pressure < 0) || (spa.pressure > 5000)) + return 12; + if ((spa.temperature <= -273) || (spa.temperature > 6000)) + return 13; + if ((spa.delta_ut1 <= -1) || (spa.delta_ut1 >= 1)) + return 17; + if ((spa.hour == 24) && (spa.minute > 0)) + return 5; + if ((spa.hour == 24) && (spa.second > 0)) + return 6; + if (Math.abs(spa.delta_t) > 8000) + return 7; + if (Math.abs(spa.timezone) > 18) + return 8; + if (Math.abs(spa.longitude) > 180) + return 9; + if (Math.abs(spa.latitude) > 90) + return 10; + if (Math.abs(spa.atmos_refract) > 5) + return 16; + if (spa.elevation < -6500000) + return 11; + return 0; +} +//=============================================================== +function julian_day(year, month, day, hour, minute, second, dut1, tz) { + var day_decimal = 0.0; + var julian_day = 0.0; + var a = 0.0; + day_decimal = day + (hour - tz + (minute + (second + dut1) / 60.0) / 60.0) / 24.0; + if (month < 3) { + month += 12; + year--; + } + julian_day = integer(365.25 * (year + 4716.0)) + integer(30.6001 * (month + 1)) + day_decimal - 1524.5; + if (julian_day > 2299160.0) { + a = integer(year / 100); + julian_day += (2 - a + integer(a / 4)); + } + return julian_day; +} +function julian_century(jd) { + return (jd - 2451545.0) / 36525.0; +} +function mean_elongation_moon_sun(jce) { + return third_order_polynomial(1.0 / 189474.0, -0.0019142, 445267.11148, 297.85036, jce); +} +function julian_ephemeris_day(jd, delta_t) { + return jd + delta_t / 86400.0; +} +function julian_ephemeris_century(jde) { + return (jde - 2451545.0) / 36525.0; +} +function julian_ephemeris_millennium(jce) { + return (jce / 10.0); +} +function earth_periodic_term_summation(terms, count, jme) { + var sum = 0; + for (var i = 0; i < count; i++) + sum += terms[i][TermsA.TERM_A] + * Math.cos(terms[i][TermsA.TERM_B] + + terms[i][TermsA.TERM_C] * jme); + return sum; +} +function earth_values(term_sum, count, jme) { + var sum = 0; + for (var i = 0; i < count; i++) + sum += term_sum[i] * Math.pow(jme, i); + sum /= 1.0e8; + return sum; +} +function earth_heliocentric_longitude(jme) { + var sum = []; + for (var i = 0; i < L_COUNT; i++) + sum[i] = earth_periodic_term_summation(L_TERMS[i], l_subcount[i], jme); + return limit_degrees(rad2deg(earth_values(sum, L_COUNT, jme))); +} +function earth_heliocentric_latitude(jme) { + var sum = []; + for (var i = 0; i < B_COUNT; i++) + sum[i] = earth_periodic_term_summation(B_TERMS[i], b_subcount[i], jme); + return rad2deg(earth_values(sum, B_COUNT, jme)); +} +function earth_radius_vector(jme) { + var sum = []; + for (var i = 0; i < R_COUNT; i++) + sum[i] = earth_periodic_term_summation(R_TERMS[i], r_subcount[i], jme); + return earth_values(sum, R_COUNT, jme); +} +function geocentric_longitude(l) { + var theta = l + 180.0; + if (theta >= 360.0) + theta -= 360.0; + return theta; +} +function geocentric_latitude(b) { + return -b; +} +function mean_anomaly_sun(jce) { + return third_order_polynomial(-1.0 / 300000.0, -0.0001603, 35999.05034, 357.52772, jce); +} +function mean_anomaly_moon(jce) { + return third_order_polynomial(1.0 / 56250.0, 0.0086972, 477198.867398, 134.96298, jce); +} +function argument_latitude_moon(jce) { + return third_order_polynomial(1.0 / 327270.0, -0.0036825, 483202.017538, 93.27191, jce); +} +function ascending_longitude_moon(jce) { + return third_order_polynomial(1.0 / 450000.0, 0.0020708, -1934.136261, 125.04452, jce); +} +function xy_term_summation(i, x) { + var sum = 0; + for (var j = 0; j < TERM_Y_COUNT; j++) + sum += x[j] * Y_TERMS[i][j]; + return sum; +} +function nutation_longitude_and_obliquity(jce, x, spa) { + var xy_term_sum; + var sum_psi = 0; + var sum_epsilon = 0; + for (var i = 0; i < Y_COUNT; i++) { + xy_term_sum = deg2rad(xy_term_summation(i, x)); + sum_psi += (PE_TERMS[i][TermsPE.TERM_PSI_A] + jce * PE_TERMS[i][TermsPE.TERM_PSI_B]) * Math.sin(xy_term_sum); + sum_epsilon += (PE_TERMS[i][TermsPE.TERM_EPS_C] + jce * PE_TERMS[i][TermsPE.TERM_EPS_D]) * Math.cos(xy_term_sum); + } + spa.del_psi = sum_psi / 36000000.0; + spa.del_epsilon = sum_epsilon / 36000000.0; +} +function ecliptic_mean_obliquity(jme) { + var u = jme / 10.0; + return 84381.448 + u * (-4680.93 + u * (-1.55 + u * (1999.25 + u * (-51.38 + u * (-249.67 + + u * (-39.05 + u * (7.12 + u * (27.87 + u * (5.79 + u * 2.45))))))))); +} +function ecliptic_true_obliquity(delta_epsilon, epsilon0) { + return delta_epsilon + epsilon0 / 3600.0; +} +function aberration_correction(r) { + return -20.4898 / (3600.0 * r); +} +function apparent_sun_longitude(theta, delta_psi, delta_tau) { + return theta + delta_psi + delta_tau; +} +function greenwich_mean_sidereal_time(jd, jc) { + return limit_degrees(280.46061837 + 360.98564736629 * (jd - 2451545.0) + + jc * jc * (0.000387933 - jc / 38710000.0)); +} +function greenwich_sidereal_time(nu0, delta_psi, epsilon) { + return nu0 + delta_psi * Math.cos(deg2rad(epsilon)); +} +function sun_equatorial_horizontal_parallax(r) { + return 8.794 / (3600.0 * r); +} +function surface_incidence_angle(zenith, azimuth_astro, azm_rotation, slope) { + var zenith_rad = deg2rad(zenith); + var slope_rad = deg2rad(slope); + return rad2deg(Math.acos(Math.cos(zenith_rad) * Math.cos(slope_rad) + + Math.sin(slope_rad) * Math.sin(zenith_rad) * Math.cos(deg2rad(azimuth_astro - azm_rotation)))); +} +function sun_mean_longitude(jme) { + return limit_degrees(280.4664567 + jme * (360007.6982779 + jme * (0.03032028 + + jme * (1 / 49931.0 + jme * (-1 / 15300.0 + jme * (-1 / 2000000.0)))))); +} +function limit_minutes(minutes) { + var limited = minutes; + if (limited < -20.0) + limited += 1440.0; + else if (limited > 20.0) + limited -= 1440.0; + return limited; +} +function eot(m, alpha, del_psi, epsilon) { + return limit_minutes(4.0 * (m - 0.0057183 - alpha + del_psi * Math.cos(deg2rad(epsilon)))); +} +function approx_sun_transit_time(alpha_zero, longitude, nu) { + return (alpha_zero - longitude - nu) / 360.0; +} +function sun_hour_angle_at_rise_set(latitude, delta_zero, h0_prime) { + var h0 = -99999; + var latitude_rad = deg2rad(latitude); + var delta_zero_rad = deg2rad(delta_zero); + var argument = (Math.sin(deg2rad(h0_prime)) - Math.sin(latitude_rad) * Math.sin(delta_zero_rad)) / + (Math.cos(latitude_rad) * Math.cos(delta_zero_rad)); + if (Math.abs(argument) <= 1) + h0 = limit_degrees180(rad2deg(Math.acos(argument))); + return h0; +} +function limit_zero2one(value) { + var limited = value - Math.floor(value); + if (limited < 0) + limited += 1.0; + return limited; +} +function approx_sun_rise_and_set(m_rts, h0) { + var h0_dfrac = h0 / 360.0; + m_rts[SunState.SUN_RISE] = limit_zero2one(m_rts[SunState.SUN_TRANSIT] - h0_dfrac); + m_rts[SunState.SUN_SET] = limit_zero2one(m_rts[SunState.SUN_TRANSIT] + h0_dfrac); + m_rts[SunState.SUN_TRANSIT] = limit_zero2one(m_rts[SunState.SUN_TRANSIT]); +} +function rts_alpha_delta_prime(ad, n) { + var a = ad[JDSign.JD_ZERO] - ad[JDSign.JD_MINUS]; + var b = ad[JDSign.JD_PLUS] - ad[JDSign.JD_ZERO]; + if (Math.abs(a) >= 2.0) + a = limit_zero2one(a); + if (Math.abs(b) >= 2.0) + b = limit_zero2one(b); + return ad[JDSign.JD_ZERO] + n * (a + b + (b - a) * n) / 2.0; +} +function limit_degrees180pm(degrees) { + var limited; + degrees /= 360.0; + limited = 360.0 * (degrees - Math.floor(degrees)); + if (limited < -180.0) + limited += 360.0; + else if (limited > 180.0) + limited -= 360.0; + return limited; +} +function limit_degrees180(degrees) { + var limited; + degrees /= 180.0; + limited = 180.0 * (degrees - Math.floor(degrees)); + if (limited < 0) + limited += 180.0; + return limited; +} +function rts_sun_altitude(latitude, delta_prime, h_prime) { + var latitude_rad = deg2rad(latitude); + var delta_prime_rad = deg2rad(delta_prime); + return rad2deg(Math.asin(Math.sin(latitude_rad) * Math.sin(delta_prime_rad) + + Math.cos(latitude_rad) * Math.cos(delta_prime_rad) * Math.cos(deg2rad(h_prime)))); +} +function sun_rise_and_set(m_rts, h_rts, delta_prime, latitude, h_prime, h0_prime, sun) { + return m_rts[sun] + (h_rts[sun] - h0_prime) / + (360.0 * Math.cos(deg2rad(delta_prime[sun])) * Math.cos(deg2rad(latitude)) * Math.sin(deg2rad(h_prime[sun]))); +} +function dayfrac_to_local_hr(dayfrac, timezone) { + return 24.0 * limit_zero2one(dayfrac + timezone / 24.0); +} +//////////////////////////////////////////////////////////////////////// +// Calculate Equation of Time (EOT) and Sun Rise, Transit, & Set (RTS) +//////////////////////////////////////////////////////////////////////// +function calculate_eot_and_sun_rise_transit_set(spa) { + var nu = 0; + var m = 0; + var h0 = 0; + var n = 0; + var alpha = []; + var delta = []; + var m_rts = []; + var nu_rts = []; + var h_rts = []; + var alpha_prime = []; + var delta_prime = []; + var h_prime = []; + var h0_prime = -1 * (SUN_RADIUS + spa.atmos_refract); + var sun_rts = copySPA(spa); + sun_rts.hour = sun_rts.minute = sun_rts.second = 0; + sun_rts.delta_ut1 = sun_rts.timezone = 0.0; + sun_rts.jd = julian_day(sun_rts.year, sun_rts.month, sun_rts.day, sun_rts.hour, sun_rts.minute, sun_rts.second, sun_rts.delta_ut1, sun_rts.timezone); + m = sun_mean_longitude(spa.jme); + spa.eot = eot(m, spa.alpha, spa.del_psi, spa.epsilon); + calculate_geocentric_sun_right_ascension_and_declination(sun_rts); + nu = sun_rts.nu; + sun_rts.delta_t = 0; + sun_rts.jd--; + for (var i = 0; i < JDSign.JD_COUNT; i++) { + calculate_geocentric_sun_right_ascension_and_declination(sun_rts); + alpha[i] = sun_rts.alpha; + delta[i] = sun_rts.delta; + sun_rts.jd++; + } + m_rts[SunState.SUN_TRANSIT] = approx_sun_transit_time(alpha[JDSign.JD_ZERO], spa.longitude, nu); + h0 = sun_hour_angle_at_rise_set(spa.latitude, delta[JDSign.JD_ZERO], h0_prime); + if (h0 >= 0) { + approx_sun_rise_and_set(m_rts, h0); + for (var i = 0; i < SunState.SUN_COUNT; i++) { + nu_rts[i] = nu + 360.985647 * m_rts[i]; + n = m_rts[i] + spa.delta_t / 86400.0; + alpha_prime[i] = rts_alpha_delta_prime(alpha, n); + delta_prime[i] = rts_alpha_delta_prime(delta, n); + h_prime[i] = limit_degrees180pm(nu_rts[i] + spa.longitude - alpha_prime[i]); + h_rts[i] = rts_sun_altitude(spa.latitude, delta_prime[i], h_prime[i]); + } + spa.srha = h_prime[SunState.SUN_RISE]; + spa.ssha = h_prime[SunState.SUN_SET]; + spa.sta = h_rts[SunState.SUN_TRANSIT]; + spa.suntransit = dayfrac_to_local_hr(m_rts[SunState.SUN_TRANSIT] - h_prime[SunState.SUN_TRANSIT] / 360.0, spa.timezone); + spa.sunrise = dayfrac_to_local_hr(sun_rise_and_set(m_rts, h_rts, delta_prime, spa.latitude, h_prime, h0_prime, SunState.SUN_RISE), spa.timezone); + spa.sunset = dayfrac_to_local_hr(sun_rise_and_set(m_rts, h_rts, delta_prime, spa.latitude, h_prime, h0_prime, SunState.SUN_SET), spa.timezone); + } + else + spa.srha = spa.ssha = spa.sta = spa.suntransit = spa.sunrise = spa.sunset = -99999; +} +//////////////////////////////////////////////////////////////////////////////////////////////// +// Calculate required SPA parameters to get the right ascension (alpha) and declination (delta) +// Note: JD must be already calculated and in structure +//////////////////////////////////////////////////////////////////////////////////////////////// +function calculate_geocentric_sun_right_ascension_and_declination(spa) { + spa.jc = julian_century(spa.jd); + spa.jde = julian_ephemeris_day(spa.jd, spa.delta_t); + spa.jce = julian_ephemeris_century(spa.jde); + spa.jme = julian_ephemeris_millennium(spa.jce); + spa.l = earth_heliocentric_longitude(spa.jme); + spa.b = earth_heliocentric_latitude(spa.jme); + spa.r = earth_radius_vector(spa.jme); + spa.theta = geocentric_longitude(spa.l); + spa.beta = geocentric_latitude(spa.b); + var x = []; + x[TermsX.TERM_X0] = spa.x0 = mean_elongation_moon_sun(spa.jce); + x[TermsX.TERM_X1] = spa.x1 = mean_anomaly_sun(spa.jce); + x[TermsX.TERM_X2] = spa.x2 = mean_anomaly_moon(spa.jce); + x[TermsX.TERM_X3] = spa.x3 = argument_latitude_moon(spa.jce); + x[TermsX.TERM_X4] = spa.x4 = ascending_longitude_moon(spa.jce); + nutation_longitude_and_obliquity(spa.jce, x, spa); + spa.epsilon0 = ecliptic_mean_obliquity(spa.jme); + spa.epsilon = ecliptic_true_obliquity(spa.del_epsilon, spa.epsilon0); + spa.del_tau = aberration_correction(spa.r); + spa.lamda = apparent_sun_longitude(spa.theta, spa.del_psi, spa.del_tau); + spa.nu0 = greenwich_mean_sidereal_time(spa.jd, spa.jc); + spa.nu = greenwich_sidereal_time(spa.nu0, spa.del_psi, spa.epsilon); + spa.alpha = geocentric_right_ascension(spa.lamda, spa.epsilon, spa.beta); + spa.delta = geocentric_declination(spa.beta, spa.epsilon, spa.lamda); +} +//Calculate SPA output values (in structure) based on input values passed in structure +function spa_calculate(spa) { + var result = validate_inputs(spa); + if (result == 0) { + spa.jd = julian_day(spa.year, spa.month, spa.day, spa.hour, spa.minute, spa.second, spa.delta_ut1, spa.timezone); + calculate_geocentric_sun_right_ascension_and_declination(spa); + spa.h = observer_hour_angle(spa.nu, spa.longitude, spa.alpha); + spa.xi = sun_equatorial_horizontal_parallax(spa.r); + var dltap = { delta_alpha: spa.del_alpha, delta_prime: spa.delta_prime }; + right_ascension_parallax_and_topocentric_dec(spa.latitude, spa.elevation, spa.xi, spa.h, spa.delta, dltap); + spa.del_alpha = dltap.delta_alpha; + spa.delta_prime = dltap.delta_prime; + spa.alpha_prime = topocentric_right_ascension(spa.alpha, spa.del_alpha); + spa.h_prime = topocentric_local_hour_angle(spa.h, spa.del_alpha); + spa.e0 = topocentric_elevation_angle(spa.latitude, spa.delta_prime, spa.h_prime); + spa.del_e = atmospheric_refraction_correction(spa.pressure, spa.temperature, spa.atmos_refract, spa.e0); + spa.e = topocentric_elevation_angle_corrected(spa.e0, spa.del_e); + spa.zenith = topocentric_zenith_angle(spa.e); + spa.azimuth_astro = topocentric_azimuth_angle_astro(spa.h_prime, spa.latitude, spa.delta_prime); + spa.azimuth = topocentric_azimuth_angle(spa.azimuth_astro); + if ((spa.function == exports.SPA_ZA_INC) || (spa.function == exports.SPA_ALL)) + spa.incidence = surface_incidence_angle(spa.zenith, spa.azimuth_astro, spa.azm_rotation, spa.slope); + if ((spa.function == exports.SPA_ZA_RTS) || (spa.function == exports.SPA_ALL)) + calculate_eot_and_sun_rise_transit_set(spa); + } + return result; +} +exports.spa_calculate = spa_calculate; diff --git a/package.json b/package.json index b5fbac8..ca79d42 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,71 @@ { "name": "nrel-spa", - "version": "1.3.0", - "description": "NREL SPA native implementation in JS", - "main": "index.js", + "version": "2.0.0", + "description": "Pure JavaScript implementation of the NREL Solar Position Algorithm (SPA). Calculates solar zenith, azimuth, sunrise, sunset, and solar noon for any location and date.", + "author": "Aric Camarata", + "license": "MIT", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + } + }, + "sideEffects": false, + "files": [ + "dist/", + "lib/", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], "scripts": { - "test": "node test.js" + "build": "tsup", + "typecheck": "tsc --noEmit", + "pretest": "tsup", + "test": "node test.mjs && node test-cjs.cjs", + "prepublishOnly": "tsup" }, "keywords": [ + "solar", "spa", "nrel", - "solar", - "calculator" + "sunrise", + "sunset", + "solar-noon", + "zenith", + "azimuth", + "solar-position", + "astronomy", + "twilight", + "javascript" ], - "author": "Ali Camarata", - "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/acamarata/nrel-spa.git" + "url": "git+https://github.com/acamarata/nrel-spa.git" }, "homepage": "https://github.com/acamarata/nrel-spa#readme", "bugs": { "url": "https://github.com/acamarata/nrel-spa/issues" + }, + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@types/node": "^25.3.0", + "tsup": "^8.5.1", + "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6f13fe1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,925 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^25.3.0 + version: 25.3.0 + tsup: + specifier: ^8.5.1 + version: 8.5.1(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.3.0': + resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@25.3.0': + dependencies: + undici-types: 7.18.2 + + acorn@8.16.0: {} + + any-promise@1.3.0: {} + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.59.0 + + fsevents@2.3.3: + optional: true + + joycon@3.1.1: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + object-assign@4.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + source-map@0.7.6: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@7.18.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a383bb2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,240 @@ +export type { + SpaOptions, + SpaResult, + SpaFormattedResult, + SpaAnglesResult, + SpaResultWithAngles, + SpaFormattedResultWithAngles, + SpaFunctionCode, +} from './types.js'; + +export { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from './types.js'; + +import { SPA_ZA_RTS } from './types.js'; +import type { + SpaOptions, + SpaResult, + SpaFormattedResult, + SpaResultWithAngles, + SpaFormattedResultWithAngles, +} from './types.js'; + +// The core SPA algorithm lives in lib/spa.js (the JS port of the NREL C source). +// In ESM builds, tsup injects a createRequire-based __require shim via the banner +// option (see tsup.config.ts). In CJS builds, require() is natively available. +declare const __require: NodeRequire; +const _load = typeof __require === 'function' ? __require : require; +const spa = _load('../lib/spa.js') as { + SpaData: new () => SpaDataInstance; + SPA_ZA_RTS: number; + spa_calculate: (data: SpaDataInstance) => number; +}; + +/** Internal SpaData instance shape from lib/spa.js */ +interface SpaDataInstance { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + timezone: number; + longitude: number; + latitude: number; + elevation: number; + pressure: number; + temperature: number; + delta_ut1: number; + delta_t: number; + slope: number; + azm_rotation: number; + atmos_refract: number; + function: number; + zenith: number; + azimuth: number; + sunrise: number; + sunset: number; + suntransit: number; + delta: number; + [key: string]: number; +} + +/** + * Validate that a value is a finite number, throwing a clear error if not. + * @internal + */ +function assertFiniteNumber(value: unknown, name: string): asserts value is number { + if (typeof value !== 'number' || !isFinite(value)) { + throw new TypeError( + `SPA: ${name} must be a finite number, got ${typeof value === 'number' ? value : typeof value}`, + ); + } +} + +/** + * Format fractional hours to HH:MM:SS string. + * Returns "N/A" for non-finite or negative values (polar night/day scenarios). + */ +export function formatTime(hours: number): string { + if (!isFinite(hours) || hours < 0) return 'N/A'; + + const totalSec = Math.round(hours * 3600); + // Wrap at 24h: values near midnight can round to 24:00:00 + const h = Math.floor(totalSec / 3600) % 24; + const rem = totalSec - Math.floor(totalSec / 3600) * 3600; + const m = Math.floor(rem / 60); + const s = rem - m * 60; + + return ( + String(h).padStart(2, '0') + ':' + + String(m).padStart(2, '0') + ':' + + String(s).padStart(2, '0') + ); +} + +/** + * Re-solve hour angles for a custom zenith angle (e.g., twilight calculations). + * + * Common angles: civil twilight 96, nautical twilight 102, astronomical twilight 108. + * + * @param base - SpaData instance with computed delta and suntransit + * @param zenithAngle - Custom zenith angle in degrees + * @returns Object with sunrise and sunset for the custom angle (NaN if no rise/set) + * @internal + */ +function adjustForCustomAngle( + base: SpaDataInstance, + zenithAngle: number, +): { sunrise: number; sunset: number } { + const phi = base.latitude * Math.PI / 180; + const delta = base.delta * Math.PI / 180; + const Z = zenithAngle * Math.PI / 180; + const cosH0 = + (Math.cos(Z) - Math.sin(phi) * Math.sin(delta)) / + (Math.cos(phi) * Math.cos(delta)); + + if (cosH0 < -1 || cosH0 > 1) { + return { sunrise: NaN, sunset: NaN }; + } + + const H0h = (Math.acos(cosH0) * 180 / Math.PI) / 15; + return { + sunrise: base.suntransit - H0h, + sunset: base.suntransit + H0h, + }; +} + +/** + * Compute solar position for the given parameters. + * + * @param date - JavaScript Date object (uses UTC components) + * @param latitude - Observer latitude in degrees (-90 to 90, negative = south) + * @param longitude - Observer longitude in degrees (-180 to 180, negative = west) + * @param timezone - Hours from UTC (e.g., -4 for EDT). Default: 0 + * @param options - Optional atmospheric and calculation parameters + * @param angles - Custom zenith angles in degrees for twilight calculations + * @returns Solar position result with computed values + */ +export function getSpa( + date: Date, + latitude: number, + longitude: number, + timezone?: number | null, + options?: SpaOptions | null, + angles?: number[], +): SpaResult | SpaResultWithAngles { + if (!(date instanceof Date) || isNaN(date.getTime())) { + throw new TypeError('SPA: date must be a valid Date object'); + } + assertFiniteNumber(latitude, 'latitude'); + assertFiniteNumber(longitude, 'longitude'); + + if (latitude < -90 || latitude > 90) { + throw new RangeError(`SPA: latitude must be between -90 and 90, got ${latitude}`); + } + if (longitude < -180 || longitude > 180) { + throw new RangeError(`SPA: longitude must be between -180 and 180, got ${longitude}`); + } + + const tz = timezone ?? 0; + const opts = options ?? {}; + + const d = new spa.SpaData(); + d.year = date.getUTCFullYear(); + d.month = date.getUTCMonth() + 1; + d.day = date.getUTCDate(); + d.hour = date.getUTCHours(); + d.minute = date.getUTCMinutes(); + d.second = date.getUTCSeconds(); + d.longitude = longitude; + d.latitude = latitude; + d.timezone = tz; + + d.elevation = opts.elevation ?? 0; + d.pressure = opts.pressure ?? 1013; + d.temperature = opts.temperature ?? 15; + d.delta_ut1 = opts.delta_ut1 ?? 0; + d.delta_t = opts.delta_t ?? 67; + d.slope = opts.slope ?? 0; + d.azm_rotation = opts.azm_rotation ?? 0; + d.atmos_refract = opts.atmos_refract ?? 0.5667; + d.function = opts.function ?? SPA_ZA_RTS; + + const rc = spa.spa_calculate(d); + if (rc !== 0) { + throw new Error(`SPA: calculation failed (error code ${rc})`); + } + + const result: SpaResult = { + zenith: d.zenith, + azimuth: d.azimuth, + sunrise: d.sunrise, + solarNoon: d.suntransit, + sunset: d.sunset, + }; + + if (angles && angles.length > 0) { + const angleResults = angles.map((Z) => adjustForCustomAngle(d, Z)); + return { + ...result, + angles: angleResults, + } as SpaResultWithAngles; + } + + return result; +} + +/** + * Same as getSpa(), but formats sunrise, solarNoon, and sunset as HH:MM:SS strings. + * Returns "N/A" for time fields during polar day or polar night. + */ +export function calcSpa( + date: Date, + latitude: number, + longitude: number, + timezone?: number | null, + options?: SpaOptions | null, + angles?: number[], +): SpaFormattedResult | SpaFormattedResultWithAngles { + const raw = getSpa(date, latitude, longitude, timezone, options, angles); + + const formatted: SpaFormattedResult = { + zenith: raw.zenith, + azimuth: raw.azimuth, + sunrise: formatTime(raw.sunrise), + solarNoon: formatTime(raw.solarNoon), + sunset: formatTime(raw.sunset), + }; + + if ('angles' in raw && raw.angles) { + return { + ...formatted, + angles: raw.angles.map((a) => ({ + sunrise: formatTime(a.sunrise), + sunset: formatTime(a.sunset), + })), + } as SpaFormattedResultWithAngles; + } + + return formatted; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ac511f3 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,71 @@ +/** SPA function codes. Control which outputs are computed. */ +export const SPA_ZA = 0 as const; +export const SPA_ZA_INC = 1 as const; +export const SPA_ZA_RTS = 2 as const; +export const SPA_ALL = 3 as const; + +export type SpaFunctionCode = typeof SPA_ZA | typeof SPA_ZA_INC | typeof SPA_ZA_RTS | typeof SPA_ALL; + +export interface SpaOptions { + /** Observer elevation in meters above sea level. Default: 0. */ + elevation?: number; + /** Atmospheric pressure in millibars. Default: 1013. */ + pressure?: number; + /** Temperature in degrees Celsius. Default: 15. */ + temperature?: number; + /** UT1-UTC correction in seconds. Default: 0. */ + delta_ut1?: number; + /** TT-UTC difference in seconds. Default: 67. */ + delta_t?: number; + /** Surface slope in degrees from horizontal. Default: 0. */ + slope?: number; + /** Surface azimuth rotation in degrees from south. Default: 0. */ + azm_rotation?: number; + /** Atmospheric refraction at sunrise/sunset in degrees. Default: 0.5667. */ + atmos_refract?: number; + /** SPA function code. Default: SPA_ZA_RTS (2). */ + function?: SpaFunctionCode; +} + +export interface SpaResult { + /** Topocentric zenith angle in degrees. */ + zenith: number; + /** Topocentric azimuth angle, eastward from north (navigational convention), in degrees. */ + azimuth: number; + /** Local sunrise time as fractional hours. */ + sunrise: number; + /** Local sun transit time (solar noon) as fractional hours. */ + solarNoon: number; + /** Local sunset time as fractional hours. */ + sunset: number; +} + +export interface SpaFormattedResult { + /** Topocentric zenith angle in degrees. */ + zenith: number; + /** Topocentric azimuth angle, eastward from north (navigational convention), in degrees. */ + azimuth: number; + /** Local sunrise time as HH:MM:SS string. "N/A" during polar day/night. */ + sunrise: string; + /** Local sun transit time as HH:MM:SS string. "N/A" during polar day/night. */ + solarNoon: string; + /** Local sunset time as HH:MM:SS string. "N/A" during polar day/night. */ + sunset: string; +} + +export interface SpaAnglesResult { + /** Sunrise time for this custom zenith angle. */ + sunrise: number; + /** Sunset time for this custom zenith angle. */ + sunset: number; +} + +export interface SpaResultWithAngles extends SpaResult { + /** Custom angle results, one per angle in the input array. */ + angles: SpaAnglesResult[]; +} + +export interface SpaFormattedResultWithAngles extends SpaFormattedResult { + /** Custom angle results with formatted times. */ + angles: Array<{ sunrise: string; sunset: string }>; +} diff --git a/test-cjs.cjs b/test-cjs.cjs new file mode 100644 index 0000000..84b4735 --- /dev/null +++ b/test-cjs.cjs @@ -0,0 +1,76 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const { + getSpa, + calcSpa, + formatTime, + SPA_ZA, + SPA_ZA_RTS, + SPA_ALL, +} = require('./dist/index.cjs'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(`${name}... PASS`); + passed++; + } catch (err) { + console.error(`${name}... FAIL: ${err.message}`); + failed++; + } +} + +// ─── Exports ───────────────────────────────────────────────────────────────── + +test('CJS: getSpa is a function', () => assert.equal(typeof getSpa, 'function')); +test('CJS: calcSpa is a function', () => assert.equal(typeof calcSpa, 'function')); +test('CJS: formatTime is a function', () => assert.equal(typeof formatTime, 'function')); +test('CJS: SPA_ZA === 0', () => assert.equal(SPA_ZA, 0)); +test('CJS: SPA_ZA_RTS === 2', () => assert.equal(SPA_ZA_RTS, 2)); +test('CJS: SPA_ALL === 3', () => assert.equal(SPA_ALL, 3)); + +// ─── Correctness ────────────────────────────────────────────────────────────── + +const nyc = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, +); + +test('CJS: NYC sunrise = 05:25:03', () => assert.equal(nyc.sunrise, '05:25:03')); +test('CJS: NYC solarNoon = 12:57:56', () => assert.equal(nyc.solarNoon, '12:57:56')); +test('CJS: NYC sunset = 20:30:35', () => assert.equal(nyc.sunset, '20:30:35')); +test('CJS: zenith is number', () => assert.equal(typeof nyc.zenith, 'number')); +test('CJS: azimuth 0-360', () => assert.ok(nyc.azimuth >= 0 && nyc.azimuth <= 360)); + +// ─── formatTime ─────────────────────────────────────────────────────────────── + +test('CJS formatTime: noon', () => assert.equal(formatTime(12), '12:00:00')); +test('CJS formatTime: negative = N/A', () => assert.equal(formatTime(-1), 'N/A')); +test('CJS formatTime: NaN = N/A', () => assert.equal(formatTime(NaN), 'N/A')); + +// ─── Custom angles ──────────────────────────────────────────────────────────── + +const twilight = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10 }, + [96, 102, 108], +); + +test('CJS angles: has angles array', () => assert.ok(Array.isArray(twilight.angles))); +test('CJS angles: three entries', () => assert.equal(twilight.angles.length, 3)); +test('CJS angles: civil < standard sunrise', () => { + const standard = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { elevation: 10 }); + assert.ok(twilight.angles[0].sunrise < standard.sunrise); +}); + +// ─── Summary ───────────────────────────────────────────────────────────────── + +console.log('---'); +console.log(`${passed + failed} tests total: ${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); diff --git a/test.js b/test.js deleted file mode 100644 index 3685716..0000000 --- a/test.js +++ /dev/null @@ -1,41 +0,0 @@ -// tested.js -const { getSpa, calcSpa } = require('./index'); - -// Use current date/time -const date = new Date(); -console.log(`Current Date: ${date.toString()}\n`); - -/* -// Example: New York with minimum params -const city = "New York"; -const lat = 40.7128; -const lng = -74.0060; -const tz = -5; // Eastern Standard Time -const params = null; -const angles = []; -*/ - -// Jakarta with all params -const city = "Jakarta"; -const lat = -6.2088; -const lng = 106.8456; -const tz = 7; // UTC+7 -const params = { - elevation: 18, // meters - temperature: 26.56, // °C - pressure: 1017 // mbar -}; -const angles = [63.435]; // example custom zenith angle - -console.log(`Test: ${city} (lat: ${lat}, lng: ${lng}, UTC${tz >= 0 ? '+' : ''}${tz})\n`); - -// Raw fractional outputs -const raw = getSpa(date, lat, lng, tz, params, angles); -// Formatted HH:MM:SS outputs -const formatted = calcSpa(date, lat, lng, tz, params, angles); - -console.log('getSpa (raw fractional values):'); -console.log(JSON.stringify(raw, null, 2), '\n'); - -console.log('calcSpa (formatted HH:MM:SS):'); -console.log(JSON.stringify(formatted, null, 2), '\n'); \ No newline at end of file diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..5c2437b --- /dev/null +++ b/test.mjs @@ -0,0 +1,257 @@ +import assert from 'node:assert/strict'; +import { + getSpa, + calcSpa, + formatTime, + SPA_ZA, + SPA_ZA_INC, + SPA_ZA_RTS, + SPA_ALL, +} from './dist/index.mjs'; + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(`${name}... PASS`); + passed++; + } catch (err) { + console.error(`${name}... FAIL: ${err.message}`); + failed++; + } +} + +function close(actual, expected, tolerance = 0.001, label = '') { + assert.ok( + Math.abs(actual - expected) <= tolerance, + `${label}: expected ${expected} ± ${tolerance}, got ${actual}`, + ); +} + +// ─── Exports ───────────────────────────────────────────────────────────────── + +test('exports: getSpa is a function', () => assert.equal(typeof getSpa, 'function')); +test('exports: calcSpa is a function', () => assert.equal(typeof calcSpa, 'function')); +test('exports: formatTime is a function', () => assert.equal(typeof formatTime, 'function')); +test('exports: SPA_ZA === 0', () => assert.equal(SPA_ZA, 0)); +test('exports: SPA_ZA_INC === 1', () => assert.equal(SPA_ZA_INC, 1)); +test('exports: SPA_ZA_RTS === 2', () => assert.equal(SPA_ZA_RTS, 2)); +test('exports: SPA_ALL === 3', () => assert.equal(SPA_ALL, 3)); + +// ─── formatTime ────────────────────────────────────────────────────────────── + +test('formatTime: zero', () => assert.equal(formatTime(0), '00:00:00')); +test('formatTime: noon', () => assert.equal(formatTime(12), '12:00:00')); +test('formatTime: 23:59:59', () => assert.equal(formatTime(23 + 59/60 + 59/3600), '23:59:59')); +test('formatTime: fractional rounding', () => assert.equal(formatTime(5.4174893), '05:25:03')); +test('formatTime: negative returns N/A', () => assert.equal(formatTime(-1), 'N/A')); +test('formatTime: NaN returns N/A', () => assert.equal(formatTime(NaN), 'N/A')); +test('formatTime: Infinity returns N/A', () => assert.equal(formatTime(Infinity), 'N/A')); +test('formatTime: midnight wrap (24h)', () => { + // A time of exactly 24.0 rounds to 00:00:00 + assert.equal(formatTime(24), '00:00:00'); +}); + +// ─── getSpa: New York summer solstice (validated against NREL C reference) ─── + +const NYC_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, +); + +test('NYC summer: sunrise ~05:25:03', () => close(NYC_SUMMER.sunrise, 5.417, 0.001, 'sunrise')); +test('NYC summer: solarNoon ~12:57:56', () => close(NYC_SUMMER.solarNoon, 12.965, 0.001, 'solarNoon')); +test('NYC summer: sunset ~20:30:35', () => close(NYC_SUMMER.sunset, 20.509, 0.001, 'sunset')); +test('NYC summer: zenith is a number', () => assert.equal(typeof NYC_SUMMER.zenith, 'number')); +test('NYC summer: azimuth 0-360', () => { + assert.ok(NYC_SUMMER.azimuth >= 0 && NYC_SUMMER.azimuth <= 360, `azimuth ${NYC_SUMMER.azimuth}`); +}); + +// ─── getSpa: New York winter solstice ──────────────────────────────────────── + +const NYC_WINTER = getSpa( + new Date('2025-12-21T00:00:00Z'), + 40.7128, -74.006, -5, + { elevation: 10, pressure: 1013, temperature: 5 }, +); + +test('NYC winter: sunrise ~07:16:41', () => close(NYC_WINTER.sunrise, 7.278, 0.001, 'sunrise')); +test('NYC winter: solarNoon ~11:54:19', () => close(NYC_WINTER.solarNoon, 11.905, 0.001, 'solarNoon')); +test('NYC winter: sunset ~16:31:56', () => close(NYC_WINTER.sunset, 16.532, 0.001, 'sunset')); + +// ─── getSpa: London summer ─────────────────────────────────────────────────── + +const LONDON_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 51.5074, -0.1278, 1, + { elevation: 11, pressure: 1013, temperature: 18 }, +); + +test('London summer: sunrise ~04:43:07', () => close(LONDON_SUMMER.sunrise, 4.718, 0.001, 'sunrise')); +test('London summer: sunset ~21:21:37', () => close(LONDON_SUMMER.sunset, 21.360, 0.001, 'sunset')); + +// ─── getSpa: Tokyo ─────────────────────────────────────────────────────────── + +const TOKYO_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 35.6895, 139.6917, 9, + { elevation: 40, pressure: 1013, temperature: 22 }, +); + +test('Tokyo summer: sunrise ~04:25:52', () => close(TOKYO_SUMMER.sunrise, 4.431, 0.001, 'sunrise')); +test('Tokyo summer: sunset ~19:00:22', () => close(TOKYO_SUMMER.sunset, 19.006, 0.001, 'sunset')); + +// ─── getSpa: Sydney winter (southern hemisphere) ───────────────────────────── + +const SYDNEY_WINTER = getSpa( + new Date('2025-06-21T00:00:00Z'), + -33.8688, 151.2093, 10, + { elevation: 58, pressure: 1013, temperature: 15 }, +); + +test('Sydney winter: sunrise ~07:00:12', () => close(SYDNEY_WINTER.sunrise, 7.003, 0.001, 'sunrise')); +test('Sydney winter: sunset ~16:53:52', () => close(SYDNEY_WINTER.sunset, 16.898, 0.001, 'sunset')); + +// ─── getSpa: Quito (equator, equinox) ──────────────────────────────────────── + +const QUITO_EQUINOX = getSpa( + new Date('2025-03-20T00:00:00Z'), + -0.1807, -78.4678, -5, + { elevation: 2850, pressure: 789, temperature: 14 }, +); + +test('Quito equinox: sunrise ~06:17:54', () => close(QUITO_EQUINOX.sunrise, 6.298, 0.001, 'sunrise')); +test('Quito equinox: sunset ~18:24:25', () => close(QUITO_EQUINOX.sunset, 18.407, 0.001, 'sunset')); + +// ─── getSpa: polar night (Tromso, arctic winter) ───────────────────────────── + +const TROMSO_POLAR = getSpa( + new Date('2025-12-21T00:00:00Z'), + 69.6492, 18.9553, 1, + { elevation: 0, pressure: 1013, temperature: -2 }, +); + +test('Tromso polar: sunrise is NaN (polar night)', () => assert.ok(isNaN(TROMSO_POLAR.sunrise) || TROMSO_POLAR.sunrise < 0 || TROMSO_POLAR.sunrise > 24)); +test('Tromso polar: zenith > 90 (sun below horizon)', () => assert.ok(TROMSO_POLAR.zenith > 90)); + +// ─── calcSpa: formatted output ─────────────────────────────────────────────── + +const NYC_FMT = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, +); + +test('calcSpa: sunrise is string', () => assert.equal(typeof NYC_FMT.sunrise, 'string')); +test('calcSpa: solarNoon is string', () => assert.equal(typeof NYC_FMT.solarNoon, 'string')); +test('calcSpa: sunset is string', () => assert.equal(typeof NYC_FMT.sunset, 'string')); +test('calcSpa: sunrise format HH:MM:SS', () => assert.match(NYC_FMT.sunrise, /^\d{2}:\d{2}:\d{2}$/)); +test('calcSpa: NYC summer sunrise = 05:25:03', () => assert.equal(NYC_FMT.sunrise, '05:25:03')); +test('calcSpa: NYC summer noon = 12:57:56', () => assert.equal(NYC_FMT.solarNoon, '12:57:56')); +test('calcSpa: NYC summer sunset = 20:30:35', () => assert.equal(NYC_FMT.sunset, '20:30:35')); +test('calcSpa: zenith is number', () => assert.equal(typeof NYC_FMT.zenith, 'number')); + +// ─── Custom angles (twilight) ──────────────────────────────────────────────── + +const NYC_TWILIGHT = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10 }, + [96, 102, 108], +); + +test('custom angles: result has angles array', () => assert.ok(Array.isArray(NYC_TWILIGHT.angles))); +test('custom angles: three entries', () => assert.equal(NYC_TWILIGHT.angles.length, 3)); +test('custom angles: civil twilight rise < standard rise', () => { + assert.ok(NYC_TWILIGHT.angles[0].sunrise < NYC_SUMMER.sunrise, 'civil rises before standard'); +}); +test('custom angles: nautical rise < civil rise', () => { + assert.ok(NYC_TWILIGHT.angles[1].sunrise < NYC_TWILIGHT.angles[0].sunrise); +}); +test('custom angles: astronomical rise < nautical rise', () => { + assert.ok(NYC_TWILIGHT.angles[2].sunrise < NYC_TWILIGHT.angles[1].sunrise); +}); + +const NYC_TWILIGHT_FMT = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10 }, + [96, 102, 108], +); + +test('calcSpa angles: formatted sunrise is string', () => { + assert.equal(typeof NYC_TWILIGHT_FMT.angles[0].sunrise, 'string'); +}); +test('calcSpa angles: HH:MM:SS format', () => { + assert.match(NYC_TWILIGHT_FMT.angles[0].sunrise, /^\d{2}:\d{2}:\d{2}$/); +}); + +// ─── Input validation ───────────────────────────────────────────────────────── + +test('validation: invalid Date throws TypeError', () => { + assert.throws(() => getSpa(new Date('invalid'), 40, -74, 0), TypeError); +}); +test('validation: non-number latitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 'bad', -74, 0), TypeError); +}); +test('validation: latitude > 90 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 91, -74, 0), RangeError); +}); +test('validation: latitude < -90 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), -91, -74, 0), RangeError); +}); +test('validation: longitude > 180 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, 181, 0), RangeError); +}); +test('validation: longitude < -180 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -181, 0), RangeError); +}); + +// ─── Defaults ──────────────────────────────────────────────────────────────── + +test('defaults: tz=null uses 0', () => { + const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, null); + assert.equal(typeof r.zenith, 'number'); +}); +test('defaults: no options arg', () => { + const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006); + assert.equal(typeof r.sunrise, 'number'); +}); +test('defaults: empty angles array returns no angles key', () => { + const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, {}, []); + assert.ok(!('angles' in r)); +}); + +// ─── Cape Town (southern hemisphere, summer) ────────────────────────────────── + +const CAPE_TOWN = getSpa( + new Date('2025-12-21T00:00:00Z'), + -33.9249, 18.4241, 2, + { elevation: 25, pressure: 1013, temperature: 18 }, +); + +test('Cape Town summer: sunrise ~05:31:55', () => close(CAPE_TOWN.sunrise, 5.532, 0.001, 'sunrise')); +test('Cape Town summer: sunset ~19:57:01', () => close(CAPE_TOWN.sunset, 19.950, 0.001, 'sunset')); + +// ─── Reykjavik (midnight sun) ───────────────────────────────────────────────── + +const REYKJAVIK = getSpa( + new Date('2025-06-21T00:00:00Z'), + 64.1466, -21.9426, 0, + { elevation: 0, pressure: 1013, temperature: 10 }, +); + +test('Reykjavik midsummer: sunrise ~02:55', () => close(REYKJAVIK.sunrise, 2.919, 0.001, 'sunrise')); +// Sunset wraps past midnight, so the raw value > 24 or suntransit is reliable +test('Reykjavik midsummer: solarNoon in range', () => { + assert.ok(REYKJAVIK.solarNoon > 12 && REYKJAVIK.solarNoon < 15); +}); + +// ─── Summary ───────────────────────────────────────────────────────────────── + +console.log('---'); +console.log(`${passed + failed} tests total: ${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6720bc6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..69972f2 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + outDir: 'dist', + splitting: false, + sourcemap: true, + target: 'es2020', + platform: 'node', + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : '.mjs', + }; + }, + banner({ format }) { + if (format === 'esm') { + return { + js: `import { createRequire as __cr } from 'node:module';\nconst __require = __cr(import.meta.url);`, + }; + } + return {}; + }, + // The core SPA algorithm lives in lib/spa.js (the JS port of the NREL C source). + // It is checked into git and ships with the package. We load it at runtime so it + // is kept external (not bundled) and resolves via the createRequire shim in ESM. + external: ['../lib/spa.js'], +});