Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tojson-and-daysuntileol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"node-version": minor
---

Add `daysUntilEOL` property and `toJSON()` method to NodeVersion
60 changes: 56 additions & 4 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -329,5 +329,57 @@ 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 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";
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();
});
});
});
40 changes: 28 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -47,6 +47,30 @@ 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 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}`,
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.
Expand Down Expand Up @@ -83,12 +107,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;
},
Expand All @@ -104,11 +123,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,
};
};

Expand Down
23 changes: 23 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>;