blob: 95961d53be1e0be014d36f6c6395eef239de610e [file]
/* 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;
};