nrel-spa/.wiki/Architecture.md

4.8 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
  • Validates options.function and throws RangeError for out-of-range codes
  • Returns NaN for sunrise, solarNoon, and sunset when the function code does not include RTS (SPA_ZA or SPA_ZA_INC)
  • Maps the flat SpaData structure to a clean output object
  • Implements adjustForCustomAngle() internally for twilight calculations
  • Provides formatTime() as a standalone export

src/types.ts exports all public TypeScript interfaces and constants:

  • SpaOptions: atmospheric and calculation parameters
  • SpaResult / SpaFormattedResult: base return types for getSpa / calcSpa
  • SpaAnglesResult / SpaFormattedAnglesResult: per-angle entries in the angles array
  • SpaResultWithAngles / SpaFormattedResultWithAngles: return types when angles is passed
  • SpaFunctionCode: union type 0 | 1 | 2 | 3
  • SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL: function code constants

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