diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e34d309..59908d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,15 +101,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Enable corepack + run: corepack enable + - name: Setup Node 24 uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm - - name: Enable corepack - run: corepack enable - - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/eslint.config.mjs b/eslint.config.mjs index 1361a88..4e5b6f1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,10 +5,17 @@ import { typescript } from '@acamarata/eslint-config'; export default [ { + files: ['src/**/*.ts'], plugins: { '@typescript-eslint': tsPlugin }, - languageOptions: { parser: tsParser }, + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, }, - ...typescript, + ...typescript.map((cfg) => ({ ...cfg, files: ['src/**/*.ts'] })), eslintConfigPrettier, { ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs', 'wasm/', 'src/spa.c', 'src/spa.h', 'validate.mjs'], diff --git a/package.json b/package.json index 1ec9c63..399aa83 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ ], "scripts": { "build:wasm": "emcc src/spa.c src/spa_wrapper.c -O3 -flto --no-entry -sMODULARIZE=1 -sEXPORT_NAME=createSpaModule -sSINGLE_FILE=1 -sEXPORTED_FUNCTIONS='[\"_spa_calculate_wrapper\",\"_spa_free_result\",\"_malloc\",\"_free\"]' -sEXPORTED_RUNTIME_METHODS='[\"cwrap\",\"getValue\"]' -sALLOW_MEMORY_GROWTH=0 -sINITIAL_MEMORY=1048576 -sSTACK_SIZE=65536 -sENVIRONMENT='node,web,worker' -sNO_FILESYSTEM=1 -sASSERTIONS=0 -sDISABLE_EXCEPTION_CATCHING=1 -sWASM_BIGINT=0 -o wasm/spa-module.js", - "build:ts": "tsup", + "build:ts": "tsup && cp dist/index.d.ts dist/index.d.mts", "build": "pnpm run build:wasm && pnpm run build:ts", "typecheck": "tsc --noEmit", "pretest": "pnpm run build:ts", @@ -74,11 +74,13 @@ }, "devDependencies": { "@acamarata/eslint-config": "^0.1.0", - "c8": "^10.1.3", "@acamarata/prettier-config": "^0.1.0", "@acamarata/tsconfig": "^0.1.0", "@eslint/js": "^10.0.1", "@types/node": "^25.3.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "c8": "^10.1.3", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdee4aa..a75d367 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: '@types/node': specifier: ^25.3.0 version: 25.3.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.56.1 + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.3)(typescript@5.9.3) c8: specifier: ^10.1.3 version: 10.1.3 diff --git a/src/index.ts b/src/index.ts index 812308e..2210ffc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,17 @@ -import type { SpaWasmModule, SpaResult, SpaFormattedResult, SpaOptions } from './types.js'; +import type { SpaWasmModule, SpaResult, SpaFormattedResult, SpaOptions } from "./types.js"; -export type { SpaOptions, SpaResult, SpaFormattedResult } from './types.js'; -export { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from './types.js'; -export type { SpaFunctionCode } from './types.js'; +export type { SpaOptions, SpaResult, SpaFormattedResult } from "./types.js"; +export { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from "./types.js"; +export type { SpaFunctionCode } from "./types.js"; -import { SPA_ALL } from './types.js'; +import { SPA_ALL } from "./types.js"; // The WASM module is Emscripten CJS output. 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 _loadModule = typeof __require === 'function' ? __require : require; -const createSpaModule: () => Promise = _loadModule('../wasm/spa-module.cjs'); +const _loadModule = typeof __require === "function" ? __require : require; +const createSpaModule: () => Promise = _loadModule("../wasm/spa-module.cjs"); // Singleton: the WASM module initializes once, all calls share it. let _module: SpaWasmModule | null = null; @@ -64,27 +64,27 @@ export function init(): Promise { _pending = createSpaModule() .then((mod: SpaWasmModule) => { _module = mod; - _calculate = mod.cwrap('spa_calculate_wrapper', 'number', [ - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', - 'number', + _calculate = mod.cwrap("spa_calculate_wrapper", "number", [ + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "number", ]) as (...args: number[]) => number; - _free = mod.cwrap('spa_free_result', null, ['number']) as (ptr: number) => void; + _free = mod.cwrap("spa_free_result", null, ["number"]) as (ptr: number) => void; _pending = null; }) .catch((err: unknown) => { @@ -111,7 +111,7 @@ export function init(): Promise { * formatTime(Infinity) // "N/A" */ export function formatTime(hours: number): string { - if (!isFinite(hours) || hours < 0) return 'N/A'; + if (!isFinite(hours) || hours < 0) return "N/A"; const totalSec = Math.round(hours * 3600); const h = Math.floor(totalSec / 3600) % 24; @@ -119,7 +119,7 @@ export function formatTime(hours: number): string { const s = totalSec % 60; return ( - String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0') + String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") ); } @@ -127,16 +127,16 @@ export function formatTime(hours: number): string { function readResult(ptr: number): SpaResult { const m = _module!; const result: SpaResult = { - zenith: m.getValue(ptr + OFFSET.zenith, 'double'), - azimuth_astro: m.getValue(ptr + OFFSET.azimuth_astro, 'double'), - azimuth: m.getValue(ptr + OFFSET.azimuth, 'double'), - incidence: m.getValue(ptr + OFFSET.incidence, 'double'), - sunrise: m.getValue(ptr + OFFSET.sunrise, 'double'), - sunset: m.getValue(ptr + OFFSET.sunset, 'double'), - suntransit: m.getValue(ptr + OFFSET.suntransit, 'double'), - sun_transit_alt: m.getValue(ptr + OFFSET.sun_transit_alt, 'double'), - eot: m.getValue(ptr + OFFSET.eot, 'double'), - error_code: m.getValue(ptr + OFFSET.error_code, 'i32'), + zenith: m.getValue(ptr + OFFSET.zenith, "double"), + azimuth_astro: m.getValue(ptr + OFFSET.azimuth_astro, "double"), + azimuth: m.getValue(ptr + OFFSET.azimuth, "double"), + incidence: m.getValue(ptr + OFFSET.incidence, "double"), + sunrise: m.getValue(ptr + OFFSET.sunrise, "double"), + sunset: m.getValue(ptr + OFFSET.sunset, "double"), + suntransit: m.getValue(ptr + OFFSET.suntransit, "double"), + sun_transit_alt: m.getValue(ptr + OFFSET.sun_transit_alt, "double"), + eot: m.getValue(ptr + OFFSET.eot, "double"), + error_code: m.getValue(ptr + OFFSET.error_code, "i32"), }; _free!(ptr); return result; @@ -147,7 +147,7 @@ function readResult(ptr: number): SpaResult { * @internal */ function assertFiniteNumber(value: unknown, name: string): asserts value is number { - if (typeof value !== 'number') { + if (typeof value !== "number") { throw new TypeError(`SPA: ${name} must be a finite number, got ${typeof value}`); } if (!isFinite(value)) { @@ -157,13 +157,13 @@ function assertFiniteNumber(value: unknown, name: string): asserts value is numb /** Field names in SpaOptions that must be finite numbers when provided. */ const NUMERIC_OPTION_FIELDS = [ - 'elevation', - 'pressure', - 'temperature', - 'delta_t', - 'slope', - 'azm_rotation', - 'atmos_refract', + "elevation", + "pressure", + "temperature", + "delta_t", + "slope", + "azm_rotation", + "atmos_refract", ] as const; /** @@ -209,10 +209,10 @@ export async function spa( ): Promise { // Input validation if (!(date instanceof Date) || isNaN(date.getTime())) { - throw new TypeError('SPA: date must be a valid Date object'); + throw new TypeError("SPA: date must be a valid Date object"); } - assertFiniteNumber(latitude, 'latitude'); - assertFiniteNumber(longitude, 'longitude'); + assertFiniteNumber(latitude, "latitude"); + assertFiniteNumber(longitude, "longitude"); if (latitude < -90 || latitude > 90) { throw new RangeError(`SPA: latitude must be between -90 and 90, got ${latitude}`); @@ -252,13 +252,13 @@ export async function spa( ); if (!ptr) { - throw new Error('SPA: memory allocation failed'); + throw new Error("SPA: memory allocation failed"); } const result = readResult(ptr); if (result.error_code !== 0) { - throw new Error('SPA: calculation failed (error code ' + result.error_code + ')'); + throw new Error("SPA: calculation failed (error code " + result.error_code + ")"); } return result; diff --git a/src/types.ts b/src/types.ts index 7c63e55..6e758e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,7 +62,7 @@ export interface SpaResult { error_code: number; } -export interface SpaFormattedResult extends Omit { +export interface SpaFormattedResult extends Omit { /** Local sunrise time as HH:MM:SS string. "N/A" during polar day/night. */ sunrise: string; /** Local sunset time as HH:MM:SS string. "N/A" during polar day/night. */