| /* This file is a part of @mdn/browser-compat-data |
| * See LICENSE file for more information. */ |
| |
| /** @import {CompatData} from '../../types/types.js' */ |
| |
| /** |
| * @typedef {object} BunVersionsResponse |
| * @property {string} $note |
| * @property {Record<string, BunReleaseInfo>} releases |
| */ |
| |
| /** |
| * @typedef {object} BunReleaseInfo |
| * @property {'current' | 'retired'} status |
| * @property {string} release_date |
| * @property {string} release_notes |
| * @property {Record<string, string>} [versions] |
| * @property {string} [revision] |
| */ |
| |
| import fs from 'node:fs/promises'; |
| import { styleText } from 'node:util'; |
| |
| import { compareVersions } from 'compare-versions'; |
| |
| import stringify from '../lib/stringify-and-order-properties.js'; |
| |
| import { |
| createOrUpdateBrowserEntry, |
| gfmNoteblock, |
| updateBrowserEntry, |
| } from './utils.js'; |
| |
| const VERSIONS_API = 'https://bun.com/versions.json'; |
| |
| /** @type {Map<string, string | undefined>} */ |
| const webkitVersionCache = new Map(); |
| |
| /** |
| * Fetches Bun version information from the versions.json endpoint. |
| * @returns {Promise<BunVersionsResponse>} The versions response object. |
| */ |
| const fetchVersions = async () => { |
| const res = await fetch(VERSIONS_API, { |
| headers: { |
| 'User-Agent': |
| 'MDN-Browser-Release-Update-Bot/1.0 (+https://developer.mozilla.org/)', |
| Accept: 'application/json', |
| }, |
| }); |
| if (!res.ok) { |
| throw new Error(`Bun versions fetch failed: HTTP ${res.status}`); |
| } |
| return /** @type {BunVersionsResponse} */ (await res.json()); |
| }; |
| |
| /** |
| * Fetches WebKit version from the Version.xcconfig file at a specific commit. |
| * @param {string} commitHash - The WebKit commit hash. |
| * @returns {Promise<string | undefined>} The WebKit version in x.y.z format, or undefined if fetch fails. |
| */ |
| const fetchWebKitVersion = async (commitHash) => { |
| if (webkitVersionCache.has(commitHash)) { |
| return webkitVersionCache.get(commitHash); |
| } |
| |
| try { |
| const url = `https://api.github.com/repos/oven-sh/WebKit/contents/Configurations/Version.xcconfig?ref=${commitHash}`; |
| |
| /** @type {Record<string, string>} */ |
| const headers = { |
| 'User-Agent': |
| 'MDN-Browser-Release-Update-Bot/1.0 (+https://developer.mozilla.org/)', |
| Accept: 'application/vnd.github.v3.raw', |
| }; |
| |
| const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; |
| if (githubToken) { |
| headers['Authorization'] = `token ${githubToken}`; |
| } |
| |
| const res = await fetch(url, { headers }); |
| |
| if (res.status === 403 || res.status === 429) { |
| const retryAfter = res.headers.get('Retry-After'); |
| const rateLimitReset = res.headers.get('X-RateLimit-Reset'); |
| |
| let waitTime = 60; |
| |
| if (retryAfter) { |
| waitTime = parseInt(retryAfter, 10); |
| } else if (rateLimitReset) { |
| const resetTime = parseInt(rateLimitReset, 10) * 1000; |
| waitTime = Math.max(1, Math.ceil((resetTime - Date.now()) / 1000)); |
| } |
| |
| console.log( |
| styleText( |
| 'yellow', |
| `Rate limited for commit ${commitHash}. Waiting ${waitTime} seconds...`, |
| ), |
| ); |
| |
| await new Promise((resolve) => setTimeout(resolve, waitTime * 1000)); |
| |
| const retryRes = await fetch(url, { headers }); |
| |
| if (!retryRes.ok) { |
| console.warn( |
| styleText( |
| 'yellow', |
| `Warning: Failed to fetch WebKit Version.xcconfig for commit ${commitHash} after retry: HTTP ${retryRes.status}`, |
| ), |
| ); |
| return undefined; |
| } |
| |
| const content = await retryRes.text(); |
| |
| const majorMatch = content.match(/MAJOR_VERSION\s*=\s*(\d+)/); |
| const minorMatch = content.match(/MINOR_VERSION\s*=\s*(\d+)/); |
| const tinyMatch = content.match(/TINY_VERSION\s*=\s*(\d+)/); |
| |
| if (majorMatch && minorMatch && tinyMatch) { |
| const version = `${majorMatch[1]}.${minorMatch[1]}.${tinyMatch[1]}`; |
| webkitVersionCache.set(commitHash, version); |
| return version; |
| } |
| |
| console.warn( |
| styleText( |
| 'yellow', |
| `Warning: Could not parse version numbers from Version.xcconfig for commit ${commitHash}`, |
| ), |
| ); |
| webkitVersionCache.set(commitHash, undefined); |
| return undefined; |
| } |
| |
| if (!res.ok) { |
| console.warn( |
| styleText( |
| 'yellow', |
| `Warning: Failed to fetch WebKit Version.xcconfig for commit ${commitHash}: HTTP ${res.status}`, |
| ), |
| ); |
| return undefined; |
| } |
| |
| const content = await res.text(); |
| |
| const majorMatch = content.match(/MAJOR_VERSION\s*=\s*(\d+)/); |
| const minorMatch = content.match(/MINOR_VERSION\s*=\s*(\d+)/); |
| const tinyMatch = content.match(/TINY_VERSION\s*=\s*(\d+)/); |
| |
| if (majorMatch && minorMatch && tinyMatch) { |
| const version = `${majorMatch[1]}.${minorMatch[1]}.${tinyMatch[1]}`; |
| webkitVersionCache.set(commitHash, version); |
| return version; |
| } |
| |
| console.warn( |
| styleText( |
| 'yellow', |
| `Warning: Could not parse version numbers from Version.xcconfig for commit ${commitHash}`, |
| ), |
| ); |
| webkitVersionCache.set(commitHash, undefined); |
| return undefined; |
| } catch (error) { |
| console.warn( |
| styleText( |
| 'yellow', |
| `Warning: Error fetching WebKit version for commit ${commitHash}: ${error}`, |
| ), |
| ); |
| webkitVersionCache.set(commitHash, undefined); |
| return undefined; |
| } |
| }; |
| |
| /** |
| * Gets Bun version info from the versions data. |
| * @param {BunReleaseInfo} versionInfo - The version info object from the API. |
| * @returns {Promise<{ webkitRev?: string; bunRevision?: string }>} An object containing webkitRev and bunRevision, if available. |
| */ |
| const getBunInfoFromVersionData = async (versionInfo) => { |
| const webkitCommitHash = versionInfo.versions?.webkit; |
| const bunRevision = versionInfo.revision; |
| /** @type {string | undefined} */ |
| let webkitRev; |
| |
| if (webkitCommitHash) { |
| webkitRev = await fetchWebKitVersion(webkitCommitHash); |
| } |
| |
| return { webkitRev, bunRevision }; |
| }; |
| |
| /** |
| * Updates the Bun releases. |
| * @param {object} options - The options. |
| * @param {'bun'} options.bcdBrowserName - The name of the browser in the BCD file. |
| * @param {string} options.bcdFile - The path to the BCD file. |
| * @param {string} options.browserName - The name of the browser. |
| * @returns {Promise<string>} The result. |
| */ |
| export const updateBunReleases = async (options) => { |
| const browser = options.bcdBrowserName; |
| |
| /** @type {string} */ |
| let fileText; |
| try { |
| fileText = await fs.readFile(options.bcdFile, 'utf-8'); |
| } catch { |
| return gfmNoteblock( |
| 'NOTE', |
| `**${options.browserName ?? 'Bun'}**: No browser data file found at ${options.bcdFile}. Add a seed file (e.g., browsers/bun.json) before running updates.`, |
| ); |
| } |
| |
| /** @type {CompatData} */ |
| const data = JSON.parse(fileText); |
| |
| let result = ''; |
| |
| /** @type {BunVersionsResponse} */ |
| let versionsData; |
| try { |
| versionsData = await fetchVersions(); |
| } catch (e) { |
| return gfmNoteblock( |
| 'WARNING', |
| `**${options.browserName ?? 'Bun'}**: Failed to fetch versions data (${e}).`, |
| ); |
| } |
| |
| /** @type {(BunReleaseInfo & { version: string })[]} */ |
| const stableReleases = []; |
| |
| /** @type {string | null} */ |
| let latestVersion = null; |
| |
| for (const [version, info] of Object.entries(versionsData.releases)) { |
| // Only include versions >= 1.0.0 |
| if (compareVersions(version, '1.0.0') < 0) { |
| continue; |
| } |
| |
| stableReleases.push({ |
| version, |
| ...info, |
| }); |
| |
| if (info.status === 'current') { |
| latestVersion = version; |
| } |
| } |
| |
| stableReleases.sort((a, b) => compareVersions(a.version, b.version)); |
| |
| if (stableReleases.length === 0) { |
| return gfmNoteblock( |
| 'NOTE', |
| `**${options.browserName ?? 'Bun'}**: No stable releases found since v1.0.0.`, |
| ); |
| } |
| |
| if (!latestVersion) { |
| latestVersion = stableReleases[stableReleases.length - 1].version; |
| } |
| |
| const latest = |
| stableReleases.find((r) => r.version === latestVersion) || |
| stableReleases[stableReleases.length - 1]; |
| |
| for (const rel of stableReleases) { |
| const status = rel.version === latest.version ? 'current' : 'retired'; |
| result += createOrUpdateBrowserEntry( |
| data, |
| browser, |
| rel.version, |
| status, |
| undefined, |
| undefined, |
| rel.release_date, |
| rel.release_notes, |
| ); |
| |
| const entry = data.browsers[browser].releases[rel.version]; |
| |
| if (entry) { |
| const { webkitRev } = await getBunInfoFromVersionData(rel); |
| |
| if (webkitRev) { |
| entry.engine = 'WebKit'; |
| entry.engine_version = webkitRev; |
| } |
| } |
| } |
| |
| const existing = Object.keys(data.browsers[browser].releases ?? {}); |
| for (const v of existing) { |
| if (v !== latest.version) { |
| result += updateBrowserEntry( |
| data, |
| browser, |
| v, |
| undefined, |
| 'retired', |
| undefined, |
| undefined, |
| ); |
| } |
| } |
| |
| await fs.writeFile(`./${options.bcdFile}`, stringify(data) + '\n'); |
| |
| if (result) { |
| result = `### Updates for ${options.browserName ?? 'Bun'}\n${result}`; |
| } |
| |
| return result; |
| }; |