From 5c30879bf9d26bd0b94cbf34e6f83f0ab83e7e07 Mon Sep 17 00:00:00 2001 From: Rodolphe Stoclin Date: Fri, 16 Jan 2026 22:34:33 +0100 Subject: [PATCH 1/3] feat: add daysUntilEOL property and toJSON method --- .changeset/tojson-and-daysuntileol.md | 5 +++ src/index.test.ts | 51 ++++++++++++++++++++++++--- src/index.ts | 34 +++++++++++------- src/types.ts | 23 ++++++++++++ 4 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 .changeset/tojson-and-daysuntileol.md diff --git a/.changeset/tojson-and-daysuntileol.md b/.changeset/tojson-and-daysuntileol.md new file mode 100644 index 00000000..5fb4d386 --- /dev/null +++ b/.changeset/tojson-and-daysuntileol.md @@ -0,0 +1,5 @@ +--- +"node-version": minor +--- + +Add `daysUntilEOL` property and `toJSON()` method to NodeVersion diff --git a/src/index.test.ts b/src/index.test.ts index 5d386cbe..983418e0 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -4,8 +4,8 @@ * MIT Licensed */ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { versions as realVersions } from "node:process"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { EOL_DATES, getVersion, version } from "./index.js"; const { mockVersion, mockRelease } = vi.hoisted(() => ({ @@ -104,9 +104,9 @@ describe("node-version", () => { expect(typeof v.build).toBe("string"); }); - test("object should have exactly 16 properties", () => { - expect(Object.keys(version)).toHaveLength(16); - expect(Object.keys(getVersion())).toHaveLength(16); + test("object should have exactly 18 properties", () => { + expect(Object.keys(version)).toHaveLength(18); + expect(Object.keys(getVersion())).toHaveLength(18); }); test("original property should start with v", () => { @@ -329,5 +329,48 @@ describe("node-version", () => { const v = getVersion(); expect(v.eolDate).toBeUndefined(); }); + + test("daysUntilEOL should be positive before EOL", () => { + vi.setSystemTime(new Date("2024-01-01")); + mockVersion.node = "20.10.0"; + const v = getVersion(); + expect(v.daysUntilEOL).toBeGreaterThan(0); + }); + + test("daysUntilEOL should be negative after EOL", () => { + vi.setSystemTime(new Date("2027-01-01")); + mockVersion.node = "20.10.0"; + const v = getVersion(); + expect(v.daysUntilEOL).toBeLessThan(0); + }); + + test("daysUntilEOL should be undefined for unknown version", () => { + mockVersion.node = "99.0.0"; + const v = getVersion(); + expect(v.daysUntilEOL).toBeUndefined(); + }); + }); + + describe("toJSON", () => { + test("toJSON returns data properties only", () => { + const v = getVersion(); + const json = v.toJSON(); + + expect(json).toHaveProperty("original"); + expect(json).toHaveProperty("major"); + expect(json).toHaveProperty("daysUntilEOL"); + expect(json).not.toHaveProperty("isAtLeast"); + expect(json).not.toHaveProperty("toJSON"); + }); + + test("JSON.stringify works correctly", () => { + const v = getVersion(); + const str = JSON.stringify(v); + const parsed = JSON.parse(str); + + expect(parsed.original).toBe(v.original); + expect(parsed.major).toBe(v.major); + expect(parsed.isAtLeast).toBeUndefined(); + }); }); }); diff --git a/src/index.ts b/src/index.ts index 625ca808..442ba178 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,9 +5,9 @@ */ import { release, versions } from "node:process"; -import type { NodeVersion } from "./types.js"; +import type { NodeVersion, NodeVersionJSON } from "./types.js"; -export type { NodeVersion }; +export type { NodeVersion, NodeVersionJSON }; /** * End-of-Life dates for Node.js major versions. @@ -47,6 +47,24 @@ export const getVersion = (): NodeVersion => { const nodeVersionParts = split.map((s) => Number(s) || 0); const major = split[0] || "0"; const eolString = EOL_DATES[major]; + const eolDate = eolString ? new Date(eolString) : undefined; + const daysUntilEOL = eolDate + ? Math.ceil((eolDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + : undefined; + + const dataProps = { + original: `v${nodeVersion}`, + short: `${split[0] || "0"}.${split[1] || "0"}`, + long: nodeVersion, + major, + minor: split[1] || "0", + build: split[2] || "0", + isLTS: !!release.lts, + ltsName: String(release.lts || "") || undefined, + isEOL: checkEOL(major), + eolDate, + daysUntilEOL, + }; /** * Compare the current node version with a target version string. @@ -83,12 +101,7 @@ export const getVersion = (): NodeVersion => { }; return { - original: `v${nodeVersion}`, - short: `${split[0] || "0"}.${split[1] || "0"}`, - long: nodeVersion, - major: major, - minor: split[1] || "0", - build: split[2] || "0", + ...dataProps, isAtLeast: (version: string): boolean => { return compareTo(version) >= 0; }, @@ -104,11 +117,8 @@ export const getVersion = (): NodeVersion => { isAtMost: (version: string): boolean => { return compareTo(version) <= 0; }, - isLTS: !!release.lts, - ltsName: String(release.lts || "") || undefined, - isEOL: checkEOL(major), - eolDate: eolString ? new Date(eolString) : undefined, toString: () => `v${nodeVersion}`, + toJSON: () => dataProps, }; }; diff --git a/src/types.ts b/src/types.ts index 4de06674..b94b0c7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,8 +93,31 @@ export interface NodeVersion { * @see https://github.com/nodejs/release#release-schedule */ eolDate: Date | undefined; + /** + * Days until EOL. Positive if in the future, negative if past. + * Undefined if the EOL date is not known. + */ + daysUntilEOL: number | undefined; /** * Returns the original version string. */ toString(): string; + /** + * Returns a JSON-serializable representation (data properties only, no methods). + */ + toJSON(): NodeVersionJSON; } + +/** + * JSON-serializable version of NodeVersion (data properties only). + */ +export type NodeVersionJSON = Omit< + NodeVersion, + | "isAtLeast" + | "is" + | "isAbove" + | "isBelow" + | "isAtMost" + | "toString" + | "toJSON" +>; From c016f745a4c36d2badd9c4ba46c08168efece3cf Mon Sep 17 00:00:00 2001 From: Rodolphe Stoclin Date: Fri, 16 Jan 2026 22:43:29 +0100 Subject: [PATCH 2/3] fix: use floor for daysUntilEOL calculation --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 442ba178..a2e71d8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,7 +49,7 @@ export const getVersion = (): NodeVersion => { const eolString = EOL_DATES[major]; const eolDate = eolString ? new Date(eolString) : undefined; const daysUntilEOL = eolDate - ? Math.ceil((eolDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + ? Math.floor((eolDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : undefined; const dataProps = { From 39d7b0ac0e36aebfee39282aae6593f56daf2ca5 Mon Sep 17 00:00:00 2001 From: Rodolphe Stoclin Date: Fri, 16 Jan 2026 22:50:53 +0100 Subject: [PATCH 3/3] fix: improve daysUntilEOL rounding logic --- src/index.test.ts | 9 +++++++++ src/index.ts | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 983418e0..9a8bba3d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -337,6 +337,15 @@ describe("node-version", () => { expect(v.daysUntilEOL).toBeGreaterThan(0); }); + test("daysUntilEOL should be positive even with less than 1 day remaining", () => { + // EOL for v20 is 2026-04-30T00:00:00Z, set time to 12 hours before + vi.setSystemTime(new Date("2026-04-29T12:00:00Z")); + mockVersion.node = "20.10.0"; + const v = getVersion(); + // Less than 1 day remaining should still be > 0 (any partial day counts) + expect(v.daysUntilEOL).toBeGreaterThan(0); + }); + test("daysUntilEOL should be negative after EOL", () => { vi.setSystemTime(new Date("2027-01-01")); mockVersion.node = "20.10.0"; diff --git a/src/index.ts b/src/index.ts index a2e71d8f..bae47183 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,9 +48,15 @@ export const getVersion = (): NodeVersion => { const major = split[0] || "0"; const eolString = EOL_DATES[major]; const eolDate = eolString ? new Date(eolString) : undefined; - const daysUntilEOL = eolDate - ? Math.floor((eolDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + const rawDays = eolDate + ? (eolDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24) : undefined; + const daysUntilEOL = + rawDays === undefined + ? undefined + : rawDays > 0 + ? Math.ceil(rawDays) + : Math.floor(rawDays); const dataProps = { original: `v${nodeVersion}`,