blob: 66549d7850956538d3d74047e9dfeb04b8bb10c2 [file] [log] [blame] [edit]
/* This file is a part of @mdn/browser-compat-data
* See LICENSE file for more information. */
import { compareVersions, compare } from 'compare-versions';
import bcd from '../../index.js';
import {
BrowserName,
SimpleSupportStatement,
SupportStatement,
} from '../../types/types.js';
import { InternalSupportBlock } from '../../types/index.js';
const { browsers } = bcd;
type Notes = string | string[] | null;
/**
*/
const matchingSafariVersions = new Map([
['1', '1'],
['1.1', '1'],
['1.2', '1'],
['1.3', '1'],
['2', '1'],
['3', '2'],
['3.1', '2'],
['4', '3.2'],
['5', '4.2'],
['5.1', '5'],
['9.1', '9.3'],
['10.1', '10.3'],
['11.1', '11.3'],
['12.1', '12.2'],
['13.1', '13.4'],
['14.1', '14.5'],
]);
/**
* Convert a version number to the matching version of the target browser
* @param targetBrowser The browser to mirror to
* @param sourceVersion The version from the source browser
* @returns The matching browser version
*/
export const getMatchingBrowserVersion = (
targetBrowser: BrowserName,
sourceVersion: string,
) => {
const browserData = browsers[targetBrowser];
const range = sourceVersion.includes('≤');
/* c8 ignore start */
if (!browserData.upstream) {
// This should never be reached
throw new Error('Browser does not have an upstream browser set.');
}
/* c8 ignore stop */
if (sourceVersion == 'preview') {
// If target browser doesn't have a preview version, map preview -> false
return browserData.preview_name ? 'preview' : false;
}
if (targetBrowser === 'safari_ios') {
// The mapping between Safari macOS and iOS releases is complicated and
// cannot be entirely derived from the WebKit versions. After Safari 15
// the versions have been the same, so map earlier versions manually
// and then assume if the versions are identical it's also a match.
const v = matchingSafariVersions.get(sourceVersion.replace('≤', ''));
if (v) {
return (range ? '≤' : '') + v;
}
if (sourceVersion.replace('≤', '') in browserData.releases) {
return sourceVersion;
}
throw new Error(`Cannot find iOS version matching Safari ${sourceVersion}`);
}
const releaseKeys = Object.keys(browserData.releases);
releaseKeys.sort(compareVersions);
const sourceRelease =
browsers[browserData.upstream].releases[sourceVersion.replace('≤', '')];
if (!sourceRelease) {
throw new Error(
`Could not find source release "${browserData.upstream} ${sourceVersion}"!`,
);
}
let previousReleaseEngine;
for (const r of releaseKeys) {
const release = browserData.releases[r];
// Add a range delimiter if there were previous releases of the downstream browser that used the same engine before this one (ex. after Edge 79)
const rangeDelimiter =
range && previousReleaseEngine == sourceRelease.engine;
// Handle mirroring for Chromium forks when upstream version is pre-Blink
const isChromeWebKitToBlink =
['chrome', 'chrome_android'].includes(browserData.upstream) &&
targetBrowser !== 'chrome_android' &&
release.engine == 'Blink' &&
sourceRelease.engine == 'WebKit';
const isMatchingVersion =
release.engine == sourceRelease.engine &&
release.engine_version &&
sourceRelease.engine_version &&
compare(release.engine_version, sourceRelease.engine_version, '>=');
if (isChromeWebKitToBlink || isMatchingVersion) {
return rangeDelimiter ? `≤${r}` : r;
}
previousReleaseEngine = release.engine;
}
return false;
};
/**
* Update the notes by mirroring the version and replacing the browser name
* @param notes The notes to update
* @param regex The regex to check and search
* @param replace The text to replace with
* @param versionMapper - Receives the source browser version and returns the target browser version.
* @returns The notes with replacement performed
*/
const updateNotes = (
notes: Notes | null,
regex: RegExp,
replace: string,
versionMapper: (v: string) => string | false,
): Notes | null => {
if (!notes) {
return null;
}
if (Array.isArray(notes)) {
return notes.map((note) =>
updateNotes(note, regex, replace, versionMapper),
) as Notes;
}
return notes
.replace(regex, replace)
.replace(
new RegExp(`(${replace}|version)\\s(\\d+)`, 'g'),
(match, p1, p2) => p1 + ' ' + versionMapper(p2),
);
};
/**
* Copy a support statement
* @param data The data to copied
* @returns The new copied object
*/
const copyStatement = (
data: SimpleSupportStatement,
): SimpleSupportStatement => {
const newData: Partial<SimpleSupportStatement> = {};
for (const i in data) {
newData[i] = data[i];
}
return newData as SimpleSupportStatement;
};
/**
* Perform mirroring of data
* @param sourceData The data to mirror from
* @param destination The destination browser
* @returns The mirrored support statement
*/
export const bumpSupport = (
sourceData: SupportStatement,
sourceBrowser: BrowserName,
destination: BrowserName,
): SupportStatement => {
if (Array.isArray(sourceData)) {
// Bump the individual support statements and filter out results with a
// falsy version_added. It's not possible for sourceData to have a falsy
// version_added (enforced by the lint) so there can be no notes or similar
// to preserve from such statements.
const newData = sourceData
.map(
(data) =>
bumpSupport(
data,
sourceBrowser,
destination,
) as SimpleSupportStatement,
)
.filter((item) => item.version_added);
switch (newData.length) {
case 0:
return { version_added: false };
case 1:
return newData[0];
default:
return newData;
}
}
const newData: SimpleSupportStatement = copyStatement(sourceData);
if (!browsers[destination].accepts_flags && newData.flags) {
// Remove flag data if the target browser doesn't accept flags
return { version_added: false };
}
if (typeof sourceData.version_added === 'string') {
newData.version_added = getMatchingBrowserVersion(
destination,
sourceData.version_added,
);
}
if (newData.version_added === false && sourceData.version_added !== false) {
// If the feature is added in an upstream version newer than available in the downstream browser, don't copy notes, etc.
return { version_added: false };
}
if (
sourceData.version_removed &&
typeof sourceData.version_removed === 'string'
) {
newData.version_removed = getMatchingBrowserVersion(
destination,
sourceData.version_removed,
);
}
if (newData.version_added === newData.version_removed) {
// If version_added and version_removed are the same, feature is unsupported
return { version_added: false };
}
if (sourceData.notes) {
const sourceBrowserName =
sourceBrowser === 'chrome'
? '(Google )?Chrome'
: `(${browsers[sourceBrowser].name})`;
const newNotes = updateNotes(
sourceData.notes,
new RegExp(`\\b${sourceBrowserName}\\b`, 'g'),
browsers[destination].name,
(v: string) => getMatchingBrowserVersion(destination, v),
);
if (newNotes) {
newData.notes = newNotes;
}
}
return newData;
};
/**
* Perform mirroring for the target browser
* @param destination The browser to mirror to
* @param data The data to mirror with
* @returns The mirrored data
*/
const mirrorSupport = (
destination: BrowserName,
data: InternalSupportBlock,
): SupportStatement => {
const upstream: BrowserName | undefined = browsers[destination].upstream;
if (!upstream) {
throw new Error(
`Upstream is not defined for ${destination}, cannot mirror!`,
);
}
let upstreamData = data[upstream] || null;
if (!upstreamData) {
throw new Error(
`The data for ${upstream} is not defined for mirroring to ${destination}, cannot mirror!`,
);
}
if (upstreamData === 'mirror') {
// Perform mirroring upstream if needed
upstreamData = mirrorSupport(upstream, data);
}
return bumpSupport(upstreamData, upstream, destination);
};
export default mirrorSupport;