diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index 1c1c19f15b77b..4dbb09c2979b3 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -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 of supporters + * @returns {Promise>} 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', + })) + ); diff --git a/apps/site/next-data/generators/vulnerabilities.mjs b/apps/site/next-data/generators/vulnerabilities.mjs index 82378a71d3577..5d2dfaa1aded8 100644 --- a/apps/site/next-data/generators/vulnerabilities.mjs +++ b/apps/site/next-data/generators/vulnerabilities.mjs @@ -1,6 +1,9 @@ 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, @@ -8,86 +11,75 @@ const RANGE_REGEX = /([<>]=?)\s*(\d+)(?:\.(\d+))?/; * * @returns {Promise} 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} */ + const data = Object.values(payload); + + /** @type {Promise */ + 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} */ - const data = Object.values(await response.json()); - - /** @type {Promise */ - 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; + }); diff --git a/apps/site/next.calendar.mjs b/apps/site/next.calendar.mjs index d32b4f5af27c3..b7cdb666fcd73 100644 --- a/apps/site/next.calendar.mjs +++ b/apps/site/next.calendar.mjs @@ -4,6 +4,7 @@ import { BASE_CALENDAR_URL, SHARED_CALENDAR_KEY, } from './next.calendar.constants.mjs'; +import { fetchWithRetry } from './util/fetch'; /** * @@ -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 ?? []); }; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index 8a8e9ec4196cb..c90c61711b7c6 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -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'; diff --git a/apps/site/types/index.ts b/apps/site/types/index.ts index 35643a647dbbe..3e0fd77eb4e11 100644 --- a/apps/site/types/index.ts +++ b/apps/site/types/index.ts @@ -16,3 +16,4 @@ export * from './download'; export * from './userAgent'; export * from './vulnerabilities'; export * from './page'; +export * from './supporters'; diff --git a/apps/site/types/supporters.ts b/apps/site/types/supporters.ts new file mode 100644 index 0000000000000..5da04e07c50ca --- /dev/null +++ b/apps/site/types/supporters.ts @@ -0,0 +1,9 @@ +export type Supporter = { + name: string; + image: string; + url: string; + profile: string; + source: T; +}; + +export type OpenCollectiveSupporter = Supporter<'opencollective'>; diff --git a/apps/site/util/fetch.ts b/apps/site/util/fetch.ts new file mode 100644 index 0000000000000..cc4b7b3786311 --- /dev/null +++ b/apps/site/util/fetch.ts @@ -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); + continue; + } + } +};