nrel-spa/.wiki/Architecture.md
Aric Camarata b44d9a958b 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)
2026-02-25 11:01:38 -05:00

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)