Skip to content
Open
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
42 changes: 19 additions & 23 deletions apps/site/next-data/generators/supportersData.mjs
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs';
import { fetchWithRetry } from '#site/util/fetch';

/**
* Fetches supporters data from Open Collective API, filters active backers,
* and maps it to the Supporters type.
*
* @returns {Promise<Array<import('#site/types/supporters')>>} Array of supporters
* @returns {Promise<Array<import('#site/types/supporters').OpenCollectiveSupporter>>} Array of supporters
*/
async function fetchOpenCollectiveData() {
const endpoint = 'https://opencollective.com/nodejs/members/all.json';

const response = await fetch(endpoint);

const payload = await response.json();

const members = payload
.filter(({ role, isActive }) => role === 'BACKER' && isActive)
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
.map(({ name, website, image, profile }) => ({
name,
image,
url: website,
profile,
source: 'opencollective',
}));

return members;
}

export default fetchOpenCollectiveData;
export default () =>
fetchWithRetry(OPENCOLLECTIVE_MEMBERS_URL)
.then(response => response.json())
.then(payload =>
payload
.filter(({ role, isActive }) => role === 'BACKER' && isActive)
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
.map(({ name, website, image, profile }) => ({
name,
image,
url: website,
profile,
source: 'opencollective',
}))
);
136 changes: 64 additions & 72 deletions apps/site/next-data/generators/vulnerabilities.mjs
Original file line number Diff line number Diff line change
@@ -1,93 +1,85 @@
import { VULNERABILITIES_URL } from '#site/next.constants.mjs';
import { fetchWithRetry } from '#site/util/fetch';

const RANGE_REGEX = /([<>]=?)\s*(\d+)(?:\.(\d+))?/;
const V0_REGEX = /^0\.\d+(\.x)?$/;
const VER_REGEX = /^\d+\.x$/;

/**
* Fetches vulnerability data from the Node.js Security Working Group repository,
* and returns it grouped by major version.
*
* @returns {Promise<import('#site/types/vulnerabilities').GroupedVulnerabilities>} Grouped vulnerabilities
*/
export default async function generateVulnerabilityData() {
const response = await fetch(VULNERABILITIES_URL);
export default () =>
fetchWithRetry(VULNERABILITIES_URL)
.then(response => response.json())
.then(payload => {
/** @type {Array<import('#site/types/vulnerabilities').RawVulnerability>} */
const data = Object.values(payload);

/** @type {Promise<import('#site/types/vulnerabilities').GroupedVulnerabilities> */
const grouped = {};

// Helper function to add vulnerability to a major version group
const addToGroup = (majorVersion, vulnerability) => {
grouped[majorVersion] ??= [];
grouped[majorVersion].push(vulnerability);
};

// Helper function to process version patterns
const processVersion = (version, vulnerability) => {
// Handle 0.X versions (pre-semver)
if (V0_REGEX.test(version)) {
addToGroup('0', vulnerability);

return;
}

// Handle simple major.x patterns (e.g., 12.x)
if (VER_REGEX.test(version)) {
const majorVersion = version.split('.')[0];

/** @type {Array<import('#site/types/vulnerabilities').RawVulnerability>} */
const data = Object.values(await response.json());

/** @type {Promise<import('#site/types/vulnerabilities').GroupedVulnerabilities> */
const grouped = {};

// Helper function to add vulnerability to a major version group
const addToGroup = (majorVersion, vulnerability) => {
grouped[majorVersion] ??= [];
grouped[majorVersion].push(vulnerability);
};

// Helper function to process version patterns
const processVersion = (version, vulnerability) => {
// Handle 0.X versions (pre-semver)
if (/^0\.\d+(\.x)?$/.test(version)) {
addToGroup('0', vulnerability);

return;
}

// Handle simple major.x patterns (e.g., 12.x)
if (/^\d+\.x$/.test(version)) {
const majorVersion = version.split('.')[0];
addToGroup(majorVersion, vulnerability);

addToGroup(majorVersion, vulnerability);
return;
}

return;
}
// Handle version ranges (>, >=, <, <=)
const rangeMatch = RANGE_REGEX.exec(version);

// Handle version ranges (>, >=, <, <=)
const rangeMatch = RANGE_REGEX.exec(version);
if (rangeMatch) {
const [, operator, majorVersion] = rangeMatch;

if (rangeMatch) {
const [, operator, majorVersion] = rangeMatch;
const majorNum = parseInt(majorVersion, 10);

const majorNum = parseInt(majorVersion, 10);
switch (operator) {
case '>=':
case '>':
case '<=':
addToGroup(majorVersion, vulnerability);

switch (operator) {
case '>=':
case '>':
case '<=':
addToGroup(majorVersion, vulnerability);
break;
case '<':
// Add to all major versions below the specified version
for (let i = majorNum - 1; i >= 0; i--) {
addToGroup(i.toString(), vulnerability);
}

break;
case '<':
// Add to all major versions below the specified version
for (let i = majorNum - 1; i >= 0; i--) {
addToGroup(i.toString(), vulnerability);
break;
}
}
};

break;
for (const { ref, ...vulnerability } of Object.values(data)) {
vulnerability.url = ref;
// Process all potential versions from the vulnerable field
const versions = vulnerability.vulnerable.split(' || ').filter(Boolean);

for (const version of versions) {
processVersion(version, vulnerability);
}
}
}
};

for (const vulnerability of Object.values(data)) {
const parsedVulnerability = {
cve: vulnerability.cve,
url: vulnerability.ref,
vulnerable: vulnerability.vulnerable,
patched: vulnerability.patched,
description: vulnerability.description,
overview: vulnerability.overview,
affectedEnvironments: vulnerability.affectedEnvironments,
severity: vulnerability.severity,
};

// Process all potential versions from the vulnerable field
const versions = parsedVulnerability.vulnerable
.split(' || ')
.filter(Boolean);

for (const version of versions) {
processVersion(version, parsedVulnerability);
}
}

return grouped;
}

return grouped;
});
3 changes: 2 additions & 1 deletion apps/site/next.calendar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BASE_CALENDAR_URL,
SHARED_CALENDAR_KEY,
} from './next.calendar.constants.mjs';
import { fetchWithRetry } from './util/fetch';

/**
*
Expand Down Expand Up @@ -33,7 +34,7 @@ export const getCalendarEvents = async (calendarId = '', maxResults = 20) => {
calendarQueryUrl.searchParams.append(key, value)
);

return fetch(calendarQueryUrl)
return fetchWithRetry(calendarQueryUrl)
.then(response => response.json())
.then(calendar => calendar.items ?? []);
};
6 changes: 6 additions & 0 deletions apps/site/next.constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,9 @@ export const EOL_VERSION_IDENTIFIER = 'End-of-life';
*/
export const VULNERABILITIES_URL =
'https://raw.githubusercontent.com/nodejs/security-wg/main/vuln/core/index.json';

/**
* The location of the OpenCollective data
*/
export const OPENCOLLECTIVE_MEMBERS_URL =
'https://opencollective.com/nodejs/members/all.json';
1 change: 1 addition & 0 deletions apps/site/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './download';
export * from './userAgent';
export * from './vulnerabilities';
export * from './page';
export * from './supporters';
9 changes: 9 additions & 0 deletions apps/site/types/supporters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type Supporter<T extends string> = {
name: string;
image: string;
url: string;
profile: string;
source: T;
};

export type OpenCollectiveSupporter = Supporter<'opencollective'>;
24 changes: 24 additions & 0 deletions apps/site/util/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { setTimeout } from 'node:timers/promises';

type RetryOptions = RequestInit & {
maxRetry?: number;
delay?: number;
};

export const fetchWithRetry = async (
url: string,
{ maxRetry = 3, delay = 100, ...options }: RetryOptions = {}
) => {
for (let i = 1; i <= maxRetry; i++) {
try {
return fetch(url, options);
} catch (e) {
if (i === maxRetry) {
throw e;
}

await setTimeout(delay);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can add a backoff

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYM?

Copy link
Member

@araujogui araujogui Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A retry with backoff pattern

continue;
}
}
};
Loading