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

4 KiB

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:

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, 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.

// 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 . API Reference . Twilight Calculations . NREL SPA Algorithm