285 lines
12 KiB
TypeScript
285 lines
12 KiB
TypeScript
// This file processes data from https://developer.mozilla.org/en-US/docs/Web
|
|
|
|
import bcd, { BrowserName, SimpleSupportStatement, SupportBlock } from '@mdn/browser-compat-data'
|
|
import { CSSFeature, CSSPrefixMap, CSSProperty, Engine, JSFeature, PrefixData, Support, SupportMap } from './index'
|
|
|
|
const supportedEnvironments: Record<string, Engine> = {
|
|
chrome: 'Chrome',
|
|
deno: 'Deno',
|
|
edge: 'Edge',
|
|
firefox: 'Firefox',
|
|
ie: 'IE',
|
|
nodejs: 'Node',
|
|
opera: 'Opera',
|
|
safari: 'Safari',
|
|
safari_ios: 'IOS',
|
|
}
|
|
|
|
const jsFeatures: Partial<Record<JSFeature, string>> = {
|
|
ClassStaticBlocks: 'javascript.classes.static.initialization_blocks',
|
|
ExportStarAs: 'javascript.statements.export.namespace',
|
|
FromBase64: 'javascript.builtins.Uint8Array.fromBase64',
|
|
ImportAssertions: 'javascript.statements.import.import_assertions',
|
|
ImportAttributes: 'javascript.statements.import.import_attributes',
|
|
ImportMeta: 'javascript.operators.import_meta',
|
|
RegexpMatchIndices: 'javascript.builtins.RegExp.hasIndices',
|
|
TopLevelAwait: 'javascript.operators.await.top_level',
|
|
}
|
|
|
|
const cssFeatures: Partial<Record<CSSFeature, string | string[]>> = {
|
|
ColorFunctions: [
|
|
'css.types.color.color',
|
|
'css.types.color.lab',
|
|
'css.types.color.lch',
|
|
'css.types.color.oklab',
|
|
'css.types.color.oklch',
|
|
],
|
|
GradientDoublePosition: [
|
|
'css.types.gradient.conic-gradient.doubleposition',
|
|
'css.types.gradient.linear-gradient.doubleposition',
|
|
'css.types.gradient.radial-gradient.doubleposition',
|
|
'css.types.gradient.repeating-linear-gradient.doubleposition',
|
|
'css.types.gradient.repeating-radial-gradient.doubleposition',
|
|
],
|
|
GradientInterpolation: [
|
|
'css.types.gradient.conic-gradient.hue_interpolation_method',
|
|
'css.types.gradient.conic-gradient.interpolation_color_space',
|
|
'css.types.gradient.linear-gradient.hue_interpolation_method',
|
|
'css.types.gradient.linear-gradient.interpolation_color_space',
|
|
'css.types.gradient.radial-gradient.hue_interpolation_method',
|
|
'css.types.gradient.radial-gradient.interpolation_color_space',
|
|
'css.types.gradient.repeating-conic-gradient.hue_interpolation_method',
|
|
'css.types.gradient.repeating-conic-gradient.interpolation_color_space',
|
|
'css.types.gradient.repeating-linear-gradient.hue_interpolation_method',
|
|
'css.types.gradient.repeating-linear-gradient.interpolation_color_space',
|
|
'css.types.gradient.repeating-radial-gradient.hue_interpolation_method',
|
|
'css.types.gradient.repeating-radial-gradient.interpolation_color_space',
|
|
],
|
|
GradientMidpoints: [
|
|
'css.types.gradient.linear-gradient.interpolation_hints',
|
|
'css.types.gradient.radial-gradient.interpolation_hints',
|
|
'css.types.gradient.repeating-linear-gradient.interpolation_hints',
|
|
'css.types.gradient.repeating-radial-gradient.interpolation_hints',
|
|
],
|
|
HexRGBA: 'css.types.color.rgb_hexadecimal_notation.alpha_hexadecimal_notation',
|
|
HWB: 'css.types.color.hwb',
|
|
InsetProperty: 'css.properties.inset',
|
|
Modern_RGB_HSL: [
|
|
'css.types.color.hsl.alpha_parameter',
|
|
'css.types.color.hsl.space_separated_parameters',
|
|
'css.types.color.rgb.alpha_parameter',
|
|
'css.types.color.rgb.float_values',
|
|
'css.types.color.rgb.space_separated_parameters',
|
|
],
|
|
Nesting: 'css.selectors.nesting',
|
|
RebeccaPurple: 'css.types.color.named-color.rebeccapurple',
|
|
}
|
|
|
|
const similarPrefixedProperty: Record<string, { prefix: string, property: string }> = {
|
|
'css.properties.mask-composite': {
|
|
prefix: '-webkit-',
|
|
property: 'css.properties.-webkit-mask-composite',
|
|
},
|
|
}
|
|
|
|
const cssPrefixFeatures: Record<string, CSSProperty> = {
|
|
'css.properties.height.stretch': 'DHeight',
|
|
'css.properties.mask': 'DMask',
|
|
'css.properties.mask-composite': 'DMaskComposite',
|
|
'css.properties.mask-image': 'DMaskImage',
|
|
'css.properties.mask-origin': 'DMaskOrigin',
|
|
'css.properties.mask-position': 'DMaskPosition',
|
|
'css.properties.mask-repeat': 'DMaskRepeat',
|
|
'css.properties.mask-size': 'DMaskSize',
|
|
'css.properties.max-height.stretch': 'DMaxHeight',
|
|
'css.properties.max-width.stretch': 'DMaxWidth',
|
|
'css.properties.min-height.stretch': 'DMinHeight',
|
|
'css.properties.min-width.stretch': 'DMinWidth',
|
|
'css.properties.text-decoration-color': 'DTextDecorationColor',
|
|
'css.properties.text-decoration-line': 'DTextDecorationLine',
|
|
'css.properties.text-decoration-skip': 'DTextDecorationSkip',
|
|
'css.properties.text-emphasis-color': 'DTextEmphasisColor',
|
|
'css.properties.text-emphasis-position': 'DTextEmphasisPosition',
|
|
'css.properties.text-emphasis-style': 'DTextEmphasisStyle',
|
|
'css.properties.user-select': 'DUserSelect',
|
|
'css.properties.width.stretch': 'DWidth',
|
|
}
|
|
|
|
const alternativeNameToPrefix: Record<string, string> = {
|
|
'-webkit-fill-available': '-webkit-',
|
|
'-moz-available': '-moz-',
|
|
}
|
|
|
|
export const js: SupportMap<JSFeature> = {} as SupportMap<JSFeature>
|
|
export const css: SupportMap<CSSFeature> = {} as SupportMap<CSSFeature>
|
|
export const cssPrefix: CSSPrefixMap = {}
|
|
|
|
const isSemver = /^\d+(?:\.\d+(?:\.\d+)?)?$/
|
|
|
|
const compareVersions = (aStr: string, bStr: string): number => {
|
|
const a = aStr.split('.')
|
|
const b = bStr.split('.')
|
|
let diff = +a[0] - +b[0]
|
|
if (diff === 0) {
|
|
diff = +(a[1] || '0') - +(b[1] || '0')
|
|
if (diff === 0) {
|
|
diff = +(a[2] || '0') - +(b[2] || '0')
|
|
}
|
|
}
|
|
return diff
|
|
}
|
|
|
|
const extractProperty = (object: any, fullKey: string): any => {
|
|
for (const key of fullKey.split('.')) {
|
|
object = object[key]
|
|
}
|
|
if (!object) throw new Error(`Failed to find "${fullKey}"`)
|
|
return object
|
|
}
|
|
|
|
const addFeatures = <F extends string>(map: SupportMap<F>, features: Partial<Record<F, string | string[]>>): void => {
|
|
for (const feature in features) {
|
|
const keys = features[feature]
|
|
const maxVersions: Partial<Record<Engine, { version: string, isSupported: boolean }>> = {}
|
|
|
|
for (const fullKey of Array.isArray(keys) ? keys : [keys]) {
|
|
const support: SupportBlock = extractProperty(bcd, fullKey).__compat.support
|
|
|
|
for (const env in support) {
|
|
const engine = supportedEnvironments[env]
|
|
|
|
if (engine) {
|
|
const entries = support[env as BrowserName]!
|
|
|
|
for (const { flags, version_added, version_removed, partial_implementation } of Array.isArray(entries) ? entries : [entries]) {
|
|
if (typeof version_added === 'string' && isSemver.test(version_added)) {
|
|
// The feature isn't considered to be supported if it was removed,
|
|
// if it requires a flag, or if it's only partially-implemented
|
|
const isSupported = (!version_removed || !flags) && !partial_implementation
|
|
const maxVersion = maxVersions[engine]
|
|
if (
|
|
!maxVersion ||
|
|
compareVersions(version_added, maxVersion.version) > 0 ||
|
|
(compareVersions(version_added, maxVersion.version) === 0 && !isSupported)
|
|
) {
|
|
maxVersions[engine] = { version: version_added, isSupported }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const engines: Partial<Record<Engine, Record<string, Support>>> = {}
|
|
for (const engine in maxVersions) {
|
|
const { version, isSupported } = maxVersions[engine as Engine]!
|
|
engines[engine as Engine] = { [version]: { force: isSupported } }
|
|
}
|
|
map[feature] = engines
|
|
}
|
|
}
|
|
|
|
addFeatures(js, jsFeatures)
|
|
addFeatures(css, cssFeatures)
|
|
|
|
for (const fullKey in cssPrefixFeatures) {
|
|
const prefixData: PrefixData[] = []
|
|
const support: SupportBlock = extractProperty(bcd, fullKey).__compat.support
|
|
|
|
for (const env in support) {
|
|
const engine = supportedEnvironments[env]
|
|
|
|
if (engine) {
|
|
const initialEntries = support[env as BrowserName]!
|
|
let entries: SimpleSupportStatement[] = Array.isArray(initialEntries) ? initialEntries : [initialEntries]
|
|
|
|
// Figure out which version this property can be used unprefixed, if any.
|
|
// This assumes that support for these CSS properties is never removed.
|
|
// This assumption is wrong (Edge removed many features when it changed
|
|
// its engine from EdgeHTML to Blink, basically becoming another browser)
|
|
// but we ignore those cases for now.
|
|
let version_unprefixed: string | undefined
|
|
for (const { prefix, flags, version_added, version_removed, alternative_name } of entries) {
|
|
if (!prefix && !alternative_name && !flags && typeof version_added === 'string' && !version_removed && isSemver.test(version_added)) {
|
|
version_unprefixed = version_added
|
|
}
|
|
}
|
|
|
|
type PrefixRange = { prefix: string, start: string, end?: string }
|
|
const ranges: PrefixRange[] = []
|
|
|
|
// The MDN dataset sometimes doesn't list prefixes if the values for the
|
|
// prefixed property are sufficiently different. In that case, we may need
|
|
// to search for the prefix information within another property instead.
|
|
const similar = similarPrefixedProperty[fullKey]
|
|
if (similar) {
|
|
const similarSupport: SupportBlock = extractProperty(bcd, similar.property).__compat.support
|
|
const similarEntries = similarSupport[env as BrowserName]
|
|
if (!similarEntries) continue
|
|
entries = Array.isArray(similarEntries) ? similarEntries : [similarEntries]
|
|
}
|
|
|
|
// Find all version ranges where a given prefix is supported
|
|
for (let i = 0; i < entries.length; i++) {
|
|
let { prefix, flags, version_added, version_removed, alternative_name } = entries[i]
|
|
|
|
if (similar) {
|
|
if (prefix) throw new Error(`Unexpected prefix "${prefix}" for similar property "${similar.property}"`)
|
|
prefix = similar.prefix
|
|
}
|
|
|
|
if ((prefix || alternative_name) && !flags && typeof version_added === 'string' && isSemver.test(version_added)) {
|
|
const range: PrefixRange = { prefix: prefix || alternativeNameToPrefix[alternative_name!], start: version_added }
|
|
let withoutPrefix: string | undefined
|
|
|
|
// The prefix is no longer needed if support for the feature was removed
|
|
if (typeof version_removed === 'string' && isSemver.test(version_removed)) {
|
|
withoutPrefix = version_removed
|
|
}
|
|
|
|
// The prefix is no longer needed if it can be used unprefixed
|
|
if (version_unprefixed && (!withoutPrefix || compareVersions(version_unprefixed, withoutPrefix) < 0)) {
|
|
withoutPrefix = version_unprefixed
|
|
}
|
|
|
|
if (withoutPrefix) {
|
|
if (compareVersions(version_added, withoutPrefix) === 0) {
|
|
// No prefix is needed if support for the property with and without the prefix was added simultaneously
|
|
continue
|
|
}
|
|
range.end = withoutPrefix
|
|
}
|
|
|
|
ranges.push(range)
|
|
}
|
|
}
|
|
|
|
// Sort earlier versions first, then sort prefixes for equal versions lexicographically
|
|
ranges.sort((a, b) => compareVersions(a.start, b.start) || +(a.prefix > b.prefix) - +(a.prefix < b.prefix))
|
|
|
|
for (let i = 0; i < ranges.length; i++) {
|
|
const { prefix, start, end } = ranges[i]
|
|
|
|
// Skip this prefix if it's entirely covered by the previous prefix.
|
|
// Sometimes engines add support for multiple prefixes at a time. For
|
|
// example, in version 12 Edge added support for both "-ms-user-select"
|
|
// and "-webkit-user-select", so we don't need to generate both.
|
|
if (i > 0) {
|
|
const prev = ranges[i - 1]
|
|
if (compareVersions(start, prev.start) >= 0 && (!prev.end || (end && compareVersions(end, prev.end) <= 0))) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
const data: PrefixData = { engine, prefix: prefix.replace(/^-|-$/g, '') }
|
|
if (end) {
|
|
data.withoutPrefix = end.split('.').map((x: string) => +x)
|
|
}
|
|
prefixData.push(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
cssPrefix[cssPrefixFeatures[fullKey]] = prefixData
|
|
}
|