mirror of
https://github.com/acamarata/nrel-spa.git
synced 2026-06-30 19:04:25 +00:00
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)
83 lines
4 KiB
Markdown
83 lines
4 KiB
Markdown
# 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)
|