Skip to content
Draft
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
7 changes: 7 additions & 0 deletions browsers/safari.json
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@
"status": "beta",
"engine": "WebKit",
"engine_version": "624.2.1"
},
"preview": {
"release_date": "2026-03-26",
"release_notes": "https://webkit.org/blog/17896/release-notes-for-safari-technology-preview-240/",
"status": "nightly",
"engine": "WebKit",
"engine_version": "625.1.10"
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion scripts/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,15 @@ export const applyMirroring = (feature) => {
const getPreviousVersion = (browser, version) => {
if (typeof version === 'string' && !version.startsWith('≤')) {
const browserVersions = Object.keys(bcd.browsers[browser].releases).sort(
compareVersions,
(a, b) => {
if (a === 'preview') {
return 1;
}
if (b === 'preview') {
return -1;
}
return compareVersions(a, b);
},
);
const currentVersionIndex = browserVersions.indexOf(version);
if (currentVersionIndex > 0) {
Expand Down
10 changes: 9 additions & 1 deletion scripts/lib/stringify-and-order-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ const doOrder = (value, order) => {
export const stringifyReleases = (releases) => {
const indentStep = ' '; // Constant with the indent step that sortStringify will use

const sortedKeys = Object.keys(releases).sort(compareVersions);
const sortedKeys = Object.keys(releases).sort((a, b) => {
if (a === 'preview') {
return 1;
}
if (b === 'preview') {
return -1;
}
return compareVersions(a, b);
});

let result = '';
for (let i = 0; i < sortedKeys.length; i++) {
Expand Down
2 changes: 2 additions & 0 deletions scripts/update-browser-releases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ const options = {
releaseNoteJSON:
'https://developer.apple.com/tutorials/data/documentation/safari-release-notes.json',
releaseNoteURLBase: 'https://developer.apple.com',
safariTPBlogFeedURL:
'https://webkit.org/blog/category/safari-technology-preview/feed/',
},
safari_ios: {
browserName: 'Safari for iOS',
Expand Down
149 changes: 148 additions & 1 deletion scripts/update-browser-releases/safari.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { styleText } from 'node:util';

import stringify from '../lib/stringify-and-order-properties.js';

import { newBrowserEntry, updateBrowserEntry } from './utils.js';
import { getRSSItems, newBrowserEntry, updateBrowserEntry } from './utils.js';

const USER_AGENT =
'MDN-Browser-Release-Update-Bot/1.0 (+https://developer.mozilla.org/)';

/**
* @typedef {object} Release
Expand Down Expand Up @@ -48,6 +51,140 @@ const extractReleaseData = (str) => {
};
};

/**
* Fetches the latest Safari Technology Preview blog post via the webkit.org RSS feed.
* @param {string} feedURL The RSS feed URL.
* @returns {Promise<{link: string, date: string}>} The URL and publication date of the latest post.
*/
const getLatestSTPBlogPost = async (feedURL) => {
const items = await getRSSItems(feedURL);
const latest = items[0];
const date = new Date(latest.pubDate).toISOString().substring(0, 10);
return { link: latest.link, date };
};

/**
* Fetches a Safari TP blog post and extracts the end commit hash from the WebKit compare URL.
* @param {string} url The blog post URL.
* @returns {Promise<string>} The end commit hash.
*/
const extractWebKitEndCommit = async (url) => {
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
if (!response.ok) {
throw new Error(`Failed to fetch blog post: HTTP ${response.status}`);
}
const html = await response.text();
const match =
/https:\/\/github\.com\/WebKit\/WebKit\/compare\/[0-9a-f]+\.\.\.([0-9a-f]+)/.exec(
html,
);
if (!match) {
throw new Error(`WebKit commit range not found in blog post: ${url}`);
}
return match[1];
};

/**
* Fetches Configurations/Version.xcconfig for a given WebKit commit and returns the version string.
* @param {string} commit The WebKit commit hash.
* @returns {Promise<string>} The WebKit version (e.g., "620.1.15").
*/
const getWebKitVersionFromCommit = async (commit) => {
const url = `https://raw.githubusercontent.com/WebKit/WebKit/${commit}/Configurations/Version.xcconfig`;
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
if (!response.ok) {
throw new Error(
`Failed to fetch Version.xcconfig: HTTP ${response.status}`,
);
}
const text = await response.text();
const major = /MAJOR_VERSION\s*=\s*(\d+)/.exec(text)?.[1];
const minor = /MINOR_VERSION\s*=\s*(\d+)/.exec(text)?.[1];
const tiny = /TINY_VERSION\s*=\s*(\d+)/.exec(text)?.[1];
if (!major || !minor || !tiny) {
throw new Error('Failed to parse WebKit version from Version.xcconfig');
}
return `${major}.${minor}.${tiny}`;
};

/**
* Applies the latest Safari Technology Preview data to an already-loaded BCD object.
* @param {*} safariBCD The in-memory BCD object to update.
* @param {*} options The options, must include bcdBrowserName and safariTPBlogFeedURL.
* @returns {Promise<string>} The log of what has been updated (empty if nothing or on error).
*/
const applyTPRelease = async (safariBCD, options) => {
//
// Get the latest Safari TP blog post via RSS
//
let blogPost;
try {
blogPost = await getLatestSTPBlogPost(options.safariTPBlogFeedURL);
} catch (e) {
console.error(
styleText('red', `\nFailed to fetch Safari TP blog feed: ${e}`),
);
return '';
}

//
// Extract the WebKit end commit from the blog post HTML
//
let commit;
try {
commit = await extractWebKitEndCommit(blogPost.link);
} catch (e) {
console.error(
styleText(
'red',
`\nFailed to extract WebKit commit from Safari TP blog post: ${e}`,
),
);
return '';
}

//
// Determine the WebKit engine version from Version.xcconfig
//
let engineVersion;
try {
engineVersion = await getWebKitVersionFromCommit(commit);
} catch (e) {
console.error(
styleText(
'red',
`\nFailed to get WebKit version for commit ${commit}: ${e}`,
),
);
return '';
}

//
// Create or update the "preview" entry
//
if (safariBCD.browsers[options.bcdBrowserName].releases['preview']) {
return updateBrowserEntry(
safariBCD,
options.bcdBrowserName,
'preview',
blogPost.date,
'nightly',
blogPost.link,
engineVersion,
);
}
return newBrowserEntry(
safariBCD,
options.bcdBrowserName,
'preview',
'nightly',
'WebKit',
blogPost.date,
blogPost.link,
engineVersion,
);
};

/**
* updateSafariFile - Update the json file listing the browser version of a safari entry
* @param {*} options The list of options for this type of Safari.
Expand Down Expand Up @@ -171,6 +308,16 @@ export const updateSafariReleases = async (options) => {
}
});

//
// Update the nightly "preview" entry from Safari Technology Preview (desktop only)
//
if (options.safariTPBlogFeedURL) {
const tpResult = await applyTPRelease(safariBCD, options);
if (tpResult) {
result += `\n#### Technology Preview\n${tpResult}`;
}
}

//
// Write the update browser's json to file
//
Expand Down
Loading