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
4 changes: 3 additions & 1 deletion .github/workflows/tests-ranges.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: "Test Suite: ranges"
on:
pull_request:
paths:
- 'ranges/*'
- 'ranges/**'
- 'core/**'
- 'aliases/**'
branches:
- main
workflow_dispatch:
Expand Down
1 change: 1 addition & 0 deletions aliases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ for (const alias of aliases) {

This module returns an `Array` of aliases:

- 'alpha' - This represents the release line in its Alpha phase, if any. Starting with Node.js 27, each release line gets Alpha releases (`vN.0.0-alpha.X.Y.Z`) before its first Current release.
- 'current' - This represents the current Node.js release line.
- 'lts/latest' - This represents the latest LTS release line.
- 'lts/active' - This represents the active LTS release lines.
Expand Down
1 change: 1 addition & 0 deletions aliases/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const aliases = [
'alpha',
'current',
'lts/latest',
'lts/active',
Expand Down
17 changes: 16 additions & 1 deletion ranges/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,31 @@ async function generateData () {
}
```

## API

### `ranges(filter, options)`

- `filter` (String or Array, optional): one or more aliases from [@nodevu/aliases](../aliases/) to limit the result to. Defaults to `'all'`, which returns every range.
- `options` (Object, optional): passed straight to [@nodevu/core](../core/), so you can set things like a custom `fetch` or `now`.

```js
const data = await ranges('alpha', { now: '2026-12-15' })
```

## Structure

This module will return a medium-sized object with the following structure, with each property being in the context of the range it's under:

- all:
- versions (Array): An array of versions.
- versions (Array): An array of versions. Leaves out Alpha releases.
- newestLts (String): The newest LTS version.
- newestSecurity (String): The newest possible security release.
- newest (String): The newest version.
- oldest (String): The oldest version.
- alpha:
- versions (Array): An array of the Alpha releases (`vN.0.0-alpha.X.Y.Z`) of the release line in its Alpha phase. Alpha releases only ever show up here, never in the other ranges.
- newest (String): The newest version.
- oldest (String): The oldest version.
- current:
- versions (Array): An array of versions.
- oldestSecurity (String): The oldest security release.
Expand Down
29 changes: 25 additions & 4 deletions ranges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const defaultRangesObject = {
newest: undefined,
oldest: undefined,
},
alpha: {
versions: [],
newest: undefined,
oldest: undefined,
},
current: {
versions: [],
oldestSecurity: undefined,
Expand Down Expand Up @@ -62,7 +67,7 @@ const defaultRangesObject = {
// If you figure one out, or there's one in the future, please feel free
// to submit a PR to update this code.

async function generateRanges(filter) {
async function generateRanges(filter, options) {
let localFilter = filter;
if (typeof localFilter === 'string') {
// check to make sure that the filter is valid. if it's not, yeet.
Expand All @@ -88,9 +93,9 @@ async function generateRanges(filter) {
localFilter = 'all';
}

// set up the data from @nodevu/core
// set up the data from @nodevu/core, passing options (like `fetch` and `now`) through

const data = await nodevu();
const data = await nodevu(options);

// deep clone the default object so we don't modify it.
//
Expand All @@ -100,9 +105,25 @@ async function generateRanges(filter) {

for (const line in data) {
for (const key in data[line].releases) {
// Alpha releases (`vN.0.0-alpha.X.Y.Z`) are pre-releases, so they only
// go into `alpha` and are kept out of every other range
if (data[line].releases[key].alpha?.isAlpha) {
if (
(localFilter === 'alpha' ||
localFilter === 'all' ||
localFilter.includes('alpha') ||
localFilter.includes('all')) &&
data[line].support?.phases.current === 'alpha'
) {
ranges.alpha.versions.push(key);
}

continue;
}

// define `all` data
if (localFilter === 'all' || localFilter.includes('all')) {
ranges.all.versions.push(key); // put every release into the all array
ranges.all.versions.push(key); // put every non-Alpha release into the all array

if (
data[line].releases[key].lts.isLts === true &&
Expand Down
8 changes: 4 additions & 4 deletions ranges/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"scripts": {
"lint": "biome check ./",
"lint:write": "biome check ./ --write",
"test": "node --test",
"coverage": "c8 node --test",
"test": "node --test test/*.test.js",
"coverage": "c8 node --test test/*.test.js",
"updates:check": "npx npm-check-updates",
"updates:update": "npx npm-check-updates -u"
},
Expand All @@ -23,8 +23,8 @@
},
"homepage": "https://github.com/cutenode/nodevu#readme",
"dependencies": {
"@nodevu/aliases": "^0.0.2",
"@nodevu/core": "^0.3.0"
"@nodevu/aliases": "^1.0.0",
"@nodevu/core": "^1.0.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
Expand Down
108 changes: 108 additions & 0 deletions ranges/test/alpha.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const { deepStrictEqual } = require('node:assert');
const ranges = require('../index');
const fixtures = require('./fixtures');
const { describe, it } = require('node:test');

// these tests use made-up data that follows the new release schedule, where
// every release line gets Alpha releases (versioned as `vN.0.0-alpha.X.Y.Z`)
// before its first Current release.
//
// see https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule
function release(version, date, lts = false) {
return { version, date, lts, files: ['linux-x64', 'src'] };
}

const index = [
release('v27.2.0', '2027-10-20', 'Placeholder'),
release('v27.1.0', '2027-06-01'),
release('v27.0.0', '2027-04-22'),
release('v27.0.0-alpha.1.0.0', '2026-12-08'),
release('v27.0.0-alpha.0.0.0', '2026-10-28'),
release('v26.13.0', '2026-10-28', 'Placeholder'),
release('v26.12.0', '2026-10-14'),
];

// the line on the new schedule goes alpha -> start -> lts -> end, with no
// maintenance phase, while the line before it keeps the old schedule. the
// real schedule.json labels the new schedule's lts date as `maintenance` until
// https://github.com/nodejs/Release/pull/1188 lands.
const schedule = {
v26: {
start: '2026-05-05',
lts: '2026-10-28',
maintenance: '2027-10-20',
end: '2029-04-30',
codename: '',
},
v27: {
alpha: '2026-10-28',
start: '2027-04-22',
lts: '2027-10-20',
end: '2030-04-30',
codename: '',
},
};

// serves the made-up data above instead of core's snapshots
const at = (now) => fixtures.at(now, { index, schedule });

describe('check to make sure that Alpha releases work as expected', async () => {
it('should leave Alpha releases out of "all"', async () => {
const data = await ranges('all', at('2026-12-15'));

deepStrictEqual(data.all.versions, ['v26.13.0', 'v26.12.0']);
deepStrictEqual(data.all.newest, 'v26.13.0');
deepStrictEqual(data.all.oldest, 'v26.12.0');
});

it('should have the Alpha releases in "alpha" during the Alpha phase', async () => {
deepStrictEqual(await ranges('alpha', at('2026-12-15')), {
alpha: {
versions: ['v27.0.0-alpha.1.0.0', 'v27.0.0-alpha.0.0.0'],
newest: 'v27.0.0-alpha.1.0.0',
oldest: 'v27.0.0-alpha.0.0.0',
},
});

const data = await ranges(undefined, at('2026-12-15'));
deepStrictEqual(data.alpha.versions, [
'v27.0.0-alpha.1.0.0',
'v27.0.0-alpha.0.0.0',
]);
});

it('should not have "alpha" outside of the Alpha phase', async () => {
for (const now of ['2026-10-20', '2027-05-01', '2027-11-01']) {
deepStrictEqual(await ranges('alpha', at(now)), {}, now);
deepStrictEqual((await ranges(undefined, at(now))).alpha, undefined, now);
}
});

it('should leave Alpha releases out of "current" once the release line is Current', async () => {
const data = await ranges(undefined, at('2027-05-01'));

deepStrictEqual(data.current.versions, ['v27.0.0']);
deepStrictEqual(data.current.newest, 'v27.0.0');
deepStrictEqual(data.all.newest, 'v27.0.0');
});

it('should leave Alpha releases out of the LTS ranges once the release line is LTS', async () => {
const data = await ranges(undefined, at('2027-11-01'));

deepStrictEqual(data['lts/latest'].versions, ['v27.2.0']);
deepStrictEqual(data['lts/active'].versions, ['v27.2.0', 'v26.13.0']);
deepStrictEqual(data['lts/maintenance'].versions, ['v26.13.0']);
});

it('should leave Alpha releases out of "eol" once the release line is End of Life', async () => {
const data = await ranges('eol', at('2030-05-01'));

deepStrictEqual(data.eol.versions, [
'v27.2.0',
'v27.1.0',
'v27.0.0',
'v26.13.0',
'v26.12.0',
]);
});
});
5 changes: 3 additions & 2 deletions ranges/test/default.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const assert = require('node:assert');
const ranges = require('../index');
const { at } = require('./fixtures');
const { describe, it } = require('node:test');

describe('data should exist as it is defined', async () => {
it('versions should have correct types on every property', async () => {
const data = await ranges();
const data = await ranges(undefined, at());
for (const key in data) {
assert.deepStrictEqual(Array.isArray(data[key].versions), true);
assert.deepStrictEqual(data[key].versions.length > 0, true);
Expand All @@ -16,7 +17,7 @@ describe('data should exist as it is defined', async () => {
});

it('both newest and oldest on every property should be the correct type', async () => {
const data = await ranges();
const data = await ranges(undefined, at());
for (const key in data) {
assert.deepStrictEqual(typeof data[key].newest, 'string');
assert.deepStrictEqual(typeof data[key].oldest, 'string');
Expand Down
31 changes: 31 additions & 0 deletions ranges/test/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const index = require('../../core/test/data/static/index.json');
const schedule = require('../../core/test/data/static/schedule.json');

// the default date for tests. it's picked so that current, active LTS,
// maintenance LTS and end of life each have a release line, and each of those
// lines already has a security release, so no range or security field is
// empty.
const now = '2025-09-01';

// returns options for `ranges` that serve `data` (core's index.json and
// schedule.json snapshots by default) instead of fetching them, only including
// releases that would be out by `now`, like the real index.json. any other URL
// throws, so tests never touch the network.
function at(date = now, data = { index, schedule }) {
return {
now: date,
fetch: async (url) => {
if (url.endsWith('/schedule.json')) {
return Response.json(data.schedule);
}

if (url.endsWith('/index.json')) {
return Response.json(data.index.filter((entry) => entry.date <= date));
}

throw new Error(`No fixture data for ${url}`);
},
};
}

module.exports = { at };
60 changes: 58 additions & 2 deletions ranges/test/known.test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
const assert = require('node:assert');
const ranges = require('../index');
const { at } = require('./fixtures');
const { describe, it } = require('node:test');

describe('check some values we know should exist', async () => {
it('should include some known, static values in "all"', async () => {
const data = await ranges();
const data = await ranges(undefined, at());
assert.deepStrictEqual(data.all.versions.includes('v19.0.0'), true); // a semver major we know exists
assert.deepStrictEqual(data.all.versions.includes('v14.21.0'), true); // an LTS release we know happened
assert.deepStrictEqual(data.all.versions.includes('v0.1.14'), true); // oldest included data in the array - this is the correct oldest data since it's technically the first entry on nodejs.org/dist/index.json
});

it('should include some known, static values in "eol"', async () => {
const data = await ranges();
const data = await ranges(undefined, at());
assert.deepStrictEqual(data.all.versions.includes('v14.21.0'), true); // an LTS release we know happened
assert.deepStrictEqual(data.eol.versions.includes('v0.8.0'), true); // oldest included data in the array - this should be fixed since it's clearly not the oldest in the dataset.
});

// pins every range's values on the fixture date, so a release line that
// lands in the wrong range fails
it('should put each release line in the right range', async () => {
const data = await ranges(undefined, at());
const summary = Object.fromEntries(
Object.entries(data).map(([alias, { versions, ...range }]) => [
alias,
range,
]),
);

assert.deepStrictEqual(summary, {
all: {
newestLts: 'v22.19.0',
newestSecurity: 'v24.4.1',
newest: 'v24.7.0',
oldest: 'v0.1.14',
},
current: {
oldestSecurity: '24.0.2',
newestSecurity: '24.4.1',
newest: 'v24.7.0',
oldest: 'v24.0.0',
},
'lts/latest': {
oldestLts: '22.11.0',
oldestSecurity: '22.4.1',
newestLts: '22.19.0',
newestSecurity: '22.17.1',
newest: 'v22.19.0',
oldest: 'v22.11.0',
},
'lts/active': {
oldestLts: '20.9.0',
oldestSecurity: '20.3.1',
newestLts: '22.19.0',
newestSecurity: '22.17.1',
newest: 'v22.19.0',
oldest: 'v20.9.0',
},
'lts/maintenance': {
oldestLts: '20.9.0',
oldestSecurity: '20.3.1',
newestLts: '20.19.4',
newestSecurity: '20.19.4',
newest: 'v20.19.4',
oldest: 'v20.9.0',
},
eol: {
newest: 'v23.11.1',
oldest: 'v0.8.0',
},
});
});
});
Loading
Loading