| /* This file is a part of @mdn/browser-compat-data |
| * See LICENSE file for more information. */ |
| |
| /** @import {RSSItem} from './utils.js' */ |
| |
| /** |
| * @typedef {object} Release |
| * @property {string} version |
| * @property {string} date |
| * @property {string} releaseNote |
| * @property {'current'} channel |
| * @property {'Blink'} engine |
| * @property {string} engineVersion |
| */ |
| |
| import fs from 'node:fs/promises'; |
| |
| import stringify from '../lib/stringify-and-order-properties.js'; |
| |
| import { |
| createOrUpdateBrowserEntry, |
| getRSSItems, |
| gfmNoteblock, |
| updateBrowserEntry, |
| } from './utils.js'; |
| |
| /** |
| * Yields RSS items across pages until there are no more items or the page limit is reached. |
| * @param {string} baseURL the base URL of the RSS feed. |
| * @param {number} maxPages the maximum number of pages to fetch. |
| * @yields {RSSItem} the RSS items. |
| */ |
| async function* feedItems(baseURL, maxPages = 1) { |
| for (let page = 1; page <= maxPages; page++) { |
| const url = page === 1 ? baseURL : `${baseURL}?paged=${page}`; |
| const items = await getRSSItems(url); |
| if (!items.length) { |
| break; |
| } |
| yield* items; |
| } |
| } |
| |
| /** |
| * Builds a Release object from an RSS item. |
| * @param {RSSItem} item the RSS item. |
| * @param {RegExp} titleVersionPattern the pattern to match the title and extract the version. |
| * @param {RegExp} descriptionEngineVersionPattern the pattern to match the description and extract the engine version. |
| * @returns {Promise<Release>} the release. |
| */ |
| const buildRelease = async ( |
| item, |
| titleVersionPattern, |
| descriptionEngineVersionPattern, |
| ) => { |
| const version = /** @type {RegExpMatchArray} */ ( |
| item.title.match(titleVersionPattern) |
| )[1]; |
| const date = new Date(item.pubDate).toISOString().split('T')[0]; |
| const releaseNote = item.link; |
| const engineVersion = await findEngineVersion( |
| item, |
| descriptionEngineVersionPattern, |
| ); |
| |
| return { |
| version, |
| date, |
| releaseNote, |
| channel: 'current', |
| engine: 'Blink', |
| engineVersion, |
| }; |
| }; |
| |
| /** |
| * Extracts the engine version from the item. |
| * @param {RSSItem} item the RSS item. |
| * @param {RegExp} engineVersionPattern the pattern to match the description or content. |
| * @returns {Promise<string>} the engine version, found |
| * @throws {Error} if engine version cannot be found |
| */ |
| const findEngineVersion = async (item, engineVersionPattern) => { |
| const descriptionMatch = item.description.match(engineVersionPattern); |
| |
| if (descriptionMatch) { |
| return descriptionMatch[1]; |
| } |
| |
| const res = await fetch(item.link); |
| |
| if (!res.ok) { |
| throw Error(`Failed to fetch: ${item.link}`); |
| } |
| |
| const html = await res.text(); |
| const text = html.replaceAll(/<[^>]*>/g, ''); |
| |
| const contentMatch = text.match(engineVersionPattern); |
| |
| if (contentMatch) { |
| return contentMatch[1]; |
| } |
| |
| return ''; |
| }; |
| |
| /** |
| * Updates the JSON files listing the Opera browser releases. |
| * @param {*} options The list of options for this type of Safari. |
| * @returns {Promise<string>} The log of what has been generated (empty if nothing) |
| */ |
| export const updateOperaReleases = async (options) => { |
| const browser = options.bcdBrowserName; |
| |
| const isDesktop = browser === 'opera'; |
| |
| let result = ''; |
| |
| const file = await fs.readFile(`${options.bcdFile}`, 'utf-8'); |
| const data = JSON.parse(file.toString()); |
| |
| // Find the version currently tracked as "current" to use as stopping condition. |
| const [currentBCDVersion] = |
| Object.entries(data.browsers[browser].releases).find( |
| ([, r]) => r.status === 'current', |
| ) ?? []; |
| |
| const newItems = /** @type {RSSItem[]} */ ([]); |
| |
| for await (const item of feedItems( |
| options.releaseFeedURL, |
| options.maxFeedPages ?? 1, |
| )) { |
| if ( |
| options.releaseFilterCreator && |
| !options.releaseFilterCreator.includes(item['dc:creator']) |
| ) { |
| continue; |
| } |
| if (!options.titleVersionPattern.test(item.title)) { |
| continue; |
| } |
| const version = /** @type {RegExpMatchArray} */ ( |
| item.title.match(options.titleVersionPattern) |
| )[1]; |
| if (version === currentBCDVersion) { |
| break; |
| } |
| newItems.push(item); |
| } |
| |
| if (!newItems.length) { |
| return gfmNoteblock( |
| 'NOTE', |
| `**${options.browserName}**: No new release announcements found in [this RSS feed](<${options.releaseFeedURL}>).`, |
| ); |
| } |
| |
| // Process releases from oldest to newest. |
| for (const item of newItems.reverse()) { |
| const release = await buildRelease( |
| item, |
| options.titleVersionPattern, |
| options.descriptionEngineVersionPattern, |
| ); |
| |
| if (!release.engineVersion) { |
| const existingEngineVersion = |
| data.browsers[browser].releases[release.version]?.engine_version; |
| if (!existingEngineVersion) { |
| result += gfmNoteblock( |
| 'CAUTION', |
| `**${options.browserName}**: No engine version found in [this blog post](<${release.releaseNote}>).`, |
| ); |
| continue; |
| } |
| result += gfmNoteblock( |
| 'WARNING', |
| `**${options.browserName}**: No engine version found in [this blog post](<${release.releaseNote}>). Using existing engine version instead.`, |
| ); |
| release.engineVersion = existingEngineVersion; |
| } |
| |
| result += createOrUpdateBrowserEntry( |
| data, |
| browser, |
| release.version, |
| release.channel, |
| release.engine, |
| release.engineVersion, |
| release.date, |
| release.releaseNote, |
| ); |
| |
| // Set previous release to "retired". |
| result += updateBrowserEntry( |
| data, |
| browser, |
| String(Number(release.version) - 1), |
| undefined, |
| 'retired', |
| undefined, |
| undefined, |
| ); |
| } |
| |
| if (isDesktop) { |
| // Determine the latest processed release (last item after oldest-to-newest reversal). |
| const latestVersion = /** @type {RegExpMatchArray} */ ( |
| newItems[newItems.length - 1].title.match(options.titleVersionPattern) |
| )[1]; |
| const latestEngineVersion = |
| data.browsers[browser].releases[latestVersion]?.engine_version; |
| |
| if (latestEngineVersion) { |
| // 1. Set next release to "beta". |
| result += createOrUpdateBrowserEntry( |
| data, |
| browser, |
| String(Number(latestVersion) + 1), |
| 'beta', |
| 'Blink', |
| String(Number(latestEngineVersion) + 1), |
| ); |
| |
| // 2. Add another release as "nightly". |
| result += createOrUpdateBrowserEntry( |
| data, |
| browser, |
| String(Number(latestVersion) + 2), |
| 'nightly', |
| 'Blink', |
| String(Number(latestEngineVersion) + 2), |
| ); |
| } |
| } |
| |
| await fs.writeFile(`./${options.bcdFile}`, stringify(data) + '\n'); |
| |
| // Returns the log |
| if (result) { |
| result = `### Updates for ${options.browserName}\n${result}`; |
| } |
| |
| return result; |
| }; |