// Run "make compat-table" to run this code // Run "make update-compat-table" to update the data sources import child_process = require('child_process') import fs = require('fs') import path = require('path') import { generateTableForJS } from './js_table' import { generateTableForCSS } from './css_table' import * as caniuse from './caniuse' import * as mdn from './mdn' export type Engine = keyof typeof engines export const engines = { Chrome: true, Deno: true, Edge: true, ES: true, Firefox: true, Hermes: true, IE: true, IOS: true, Node: true, Opera: true, Rhino: true, Safari: true, } export type JSFeature = keyof typeof jsFeatures export const jsFeatures = { ArbitraryModuleNamespaceNames: true, ArraySpread: true, Arrow: true, AsyncAwait: true, AsyncGenerator: true, Bigint: true, Class: true, ClassField: true, ClassPrivateAccessor: true, ClassPrivateBrandCheck: true, ClassPrivateField: true, ClassPrivateMethod: true, ClassPrivateStaticAccessor: true, ClassPrivateStaticField: true, ClassPrivateStaticMethod: true, ClassStaticBlocks: true, ClassStaticField: true, ConstAndLet: true, Decorators: true, DefaultArgument: true, Destructuring: true, DynamicImport: true, ExponentOperator: true, ExportStarAs: true, ForAwait: true, ForOf: true, FromBase64: true, FunctionNameConfigurable: true, FunctionOrClassPropertyAccess: true, Generator: true, Hashbang: true, ImportAssertions: true, ImportAttributes: true, ImportDefer: true, ImportMeta: true, ImportSource: true, InlineScript: true, LogicalAssignment: true, NestedRestBinding: true, NewTarget: true, NodeColonPrefixImport: true, NodeColonPrefixRequire: true, NullishCoalescing: true, ObjectAccessors: true, ObjectExtensions: true, ObjectRestSpread: true, OptionalCatchBinding: true, OptionalChain: true, RegexpDotAllFlag: true, RegexpLookbehindAssertions: true, RegexpMatchIndices: true, RegexpNamedCaptureGroups: true, RegexpSetNotation: true, RegexpStickyAndUnicodeFlags: true, RegexpUnicodePropertyEscapes: true, RestArgument: true, TemplateLiteral: true, TopLevelAwait: true, TypeofExoticObjectIsObject: true, UnicodeEscapes: true, Using: true, } export type CSSFeature = keyof typeof cssFeatures export const cssFeatures = { ColorFunctions: true, GradientDoublePosition: true, GradientInterpolation: true, GradientMidpoints: true, HexRGBA: true, HWB: true, InlineStyle: true, InsetProperty: true, IsPseudoClass: true, MediaRange: true, Modern_RGB_HSL: true, Nesting: true, RebeccaPurple: true, } export type CSSProperty = keyof typeof cssProperties export const cssProperties = { DAppearance: true, DBackdropFilter: true, DBackgroundClip: true, DBoxDecorationBreak: true, DClipPath: true, DFontKerning: true, DHeight: true, DHyphens: true, DInitialLetter: true, DMask: true, DMaskComposite: true, DMaskImage: true, DMaskOrigin: true, DMaskPosition: true, DMaskRepeat: true, DMaskSize: true, DMaxHeight: true, DMaxWidth: true, DMinHeight: true, DMinWidth: true, DPosition: true, DPrintColorAdjust: true, DTabSize: true, DTextDecorationColor: true, DTextDecorationLine: true, DTextDecorationSkip: true, DTextEmphasisColor: true, DTextEmphasisPosition: true, DTextEmphasisStyle: true, DTextOrientation: true, DTextSizeAdjust: true, DUserSelect: true, DWidth: true, } export interface Support { force?: boolean passed?: number failed?: Set } export interface VersionRange { start: number[] end?: number[] } export interface PrefixData { engine: Engine prefix: string withoutPrefix?: number[] } export type SupportMap = Record>>> export type VersionRangeMap = Partial>>> export type WhyNotMap = Partial>>> export type CSSPrefixMap = Partial> const compareVersions = (a: number[], b: number[]): number => { let diff = a[0] - b[0] if (!diff) { diff = (a[1] || 0) - (b[1] || 0) if (!diff) { diff = (a[2] || 0) - (b[2] || 0) } } return diff } const mergeSupportMaps = (to: SupportMap, from: SupportMap): void => { for (const feature in from) { const fromEngines = from[feature as F] const toEngines = to[feature as F] || (to[feature as F] = {}) for (const engine in fromEngines) { const fromVersions = fromEngines[engine as Engine] const toVersions = toEngines[engine as Engine] || (toEngines[engine as Engine] = {}) for (const version in fromVersions) { if (version in toVersions) { throw new Error(`Merge conflict with feature=${feature} engine=${engine} version=${version}`) } toVersions[version] = fromVersions[version] } } } } const mergePrefixMaps = (to: CSSPrefixMap, from: CSSPrefixMap): void => { for (const property in from) { if (property in to) { throw new Error(`Merge conflict with property=${property}`) } to[property as CSSProperty] = from[property as CSSProperty] } } const supportMapToVersionRanges = (supportMap: SupportMap): [VersionRangeMap, WhyNotMap] => { const versionRangeMap: VersionRangeMap = {} const whyNotMap: WhyNotMap = {} for (const feature in supportMap) { const engines = supportMap[feature as F] const featureMap: Partial> = {} const whyNotByEngine: Partial> = {} // Compute the maximum number of tests that any one engine has passed let maxPassed = 0 for (const engine in engines) { const versions = engines[engine as Engine] for (const version in versions) { const { passed } = versions[version] if (passed && passed > maxPassed) maxPassed = passed } } for (const engine in engines) { const versions = engines[engine as Engine] const sortedVersions: { version: number[], supported: boolean, failed?: Set }[] = [] for (const version in versions) { const { force, passed, failed } = versions[version] const parsed = version.split('.').map(x => +x) sortedVersions.push({ version: parsed, supported: force !== void 0 ? force : // If no test failed but less than the maximum number of tests passed, // that means we have partial data (some tests have never been run for // those versions). This happens for really old browser versions that // people can't even run anymore. We conservatively consider this // feature to be unsupported if not all tests were run, since it could // be dangerous to assume otherwise. !failed && passed === maxPassed, failed, }) } sortedVersions.sort((a, b) => compareVersions(a.version, b.version)) if (sortedVersions.length) { const last = sortedVersions[sortedVersions.length - 1] if (last.failed) whyNotByEngine[engine as Engine] = [...last.failed].sort() } const versionRanges: VersionRange[] = [] let i = 0 while (i < sortedVersions.length) { const { version, supported } = sortedVersions[i++] if (supported) { while (i < sortedVersions.length && sortedVersions[i].supported) { i++ } const range: VersionRange = { start: version } if (i < sortedVersions.length) range.end = sortedVersions[i].version versionRanges.push(range) } } // The target is typically used to mean "make sure it works in this // version and later". So we just take the last version range here. // // However, we make an exception for node since people sometimes use // the target to build for the version of node that they currently // have. Node has a discontiguous version range for the support of // several features that people want to use. if (versionRanges.length && engine as Engine !== 'Node') { if (versionRanges[versionRanges.length - 1].end) { // We say this engine doesn't support this feature at all if // the feature is broken in the latest version of this engine. // This sometimes happens when engines deliberately decide to // not implement a feature of the language (e.g. Hermes). continue } // Otherwise, only consider this feature to be supported for the // last version range (i.e. the earliest version for which all // later versions support this feature). Delete all version ranges // before the last version range. versionRanges.splice(0, versionRanges.length - 1) } if (versionRanges.length) { featureMap[engine as Engine] = versionRanges } } versionRangeMap[feature as F] = featureMap whyNotMap[feature as F] = whyNotByEngine } return [versionRangeMap, whyNotMap] } const installGitHubDependencies = (): void => { const updateCommits = process.argv.includes('--update') const jsonPath = path.join(__dirname, 'package.json') const jsonData = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) Object.keys(jsonData.githubDependencies).forEach(repo => { const fullPath = path.join(__dirname, 'repos', repo) if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }) child_process.execFileSync('git', ['clone', '-b', 'gh-pages', `https://github.com/${repo}.git`, fullPath], { cwd: fullPath, stdio: 'inherit' }) } child_process.execFileSync('git', ['fetch'], { cwd: fullPath, stdio: 'inherit' }) if (updateCommits) { child_process.execFileSync('git', ['reset', '--hard', '--quiet', 'origin/gh-pages'], { cwd: fullPath, stdio: 'inherit' }) const commit = child_process.execFileSync('git', ['rev-parse', 'HEAD'], { cwd: fullPath }).toString().trim() jsonData.githubDependencies[repo] = commit } else { const commit = jsonData.githubDependencies[repo] child_process.execFileSync('git', ['reset', '--hard', '--quiet', commit], { cwd: fullPath, stdio: 'inherit' }) } }) if (updateCommits) { fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2) + '\n') } } // These dependencies are not published on npm and are instead pulled from GitHub (but pinned to a specific commit) installGitHubDependencies() import('./compat-table').then(compatTable => { const js: SupportMap = {} as SupportMap for (const feature in jsFeatures) js[feature as JSFeature] = {} mergeSupportMaps(js, compatTable.js) mergeSupportMaps(js, caniuse.js) mergeSupportMaps(js, mdn.js) // ES5 features js.ObjectAccessors.ES = { 5: { force: true } } js.ObjectAccessors.Node = { '0.4': { force: true } } // "node-compat-table" doesn't appear to cover ES5 features... // ES6/ES2015 features js.ArraySpread.ES = { 2015: { force: true } } js.Arrow.ES = { 2015: { force: true } } js.Class.ES = { 2015: { force: true } } js.ConstAndLet.ES = { 2015: { force: true } } js.DefaultArgument.ES = { 2015: { force: true } } js.Destructuring.ES = { 2015: { force: true } } js.DynamicImport.ES = { 2015: { force: true } } js.ForOf.ES = { 2015: { force: true } } js.FunctionNameConfigurable.ES = { 2015: { force: true } } js.Generator.ES = { 2015: { force: true } } js.NewTarget.ES = { 2015: { force: true } } js.ObjectExtensions.ES = { 2015: { force: true } } js.RegexpStickyAndUnicodeFlags.ES = { 2015: { force: true } } js.RestArgument.ES = { 2015: { force: true } } js.TemplateLiteral.ES = { 2015: { force: true } } js.UnicodeEscapes.ES = { 2015: { force: true } } // ES2016 features js.ExponentOperator.ES = { 2016: { force: true } } js.NestedRestBinding.ES = { 2016: { force: true } } // ES2017 features js.AsyncAwait.ES = { 2017: { force: true } } // ES2018 features js.AsyncGenerator.ES = { 2018: { force: true } } js.ForAwait.ES = { 2018: { force: true } } js.ObjectRestSpread.ES = { 2018: { force: true } } js.RegexpDotAllFlag.ES = { 2018: { force: true } } js.RegexpLookbehindAssertions.ES = { 2018: { force: true } } js.RegexpNamedCaptureGroups.ES = { 2018: { force: true } } js.RegexpUnicodePropertyEscapes.ES = { 2018: { force: true } } // ES2019 features js.OptionalCatchBinding.ES = { 2019: { force: true } } // ES2020 features js.Bigint.ES = { 2020: { force: true } } js.ExportStarAs.ES = { 2020: { force: true } } js.ImportMeta.ES = { 2020: { force: true } } js.NullishCoalescing.ES = { 2020: { force: true } } js.OptionalChain.ES = { 2020: { force: true } } js.TypeofExoticObjectIsObject.ES = { 2020: { force: true } } // https://github.com/tc39/ecma262/pull/1441 // ES2021 features js.LogicalAssignment.ES = { 2021: { force: true } } // ES2022 features js.ClassField.ES = { 2022: { force: true } } js.ClassPrivateAccessor.ES = { 2022: { force: true } } js.ClassPrivateBrandCheck.ES = { 2022: { force: true } } js.ClassPrivateField.ES = { 2022: { force: true } } js.ClassPrivateMethod.ES = { 2022: { force: true } } js.ClassPrivateStaticAccessor.ES = { 2022: { force: true } } js.ClassPrivateStaticField.ES = { 2022: { force: true } } js.ClassPrivateStaticMethod.ES = { 2022: { force: true } } js.ClassStaticBlocks.ES = { 2022: { force: true } } js.ClassStaticField.ES = { 2022: { force: true } } js.TopLevelAwait.ES = { 2022: { force: true } } js.ArbitraryModuleNamespaceNames.ES = { 2022: { force: true } } js.RegexpMatchIndices.ES = { 2022: { force: true } } // ES2023 features js.Hashbang.ES = { 2023: { force: true } } // ES2024 features js.RegexpSetNotation.ES = { 2024: { force: true } } // This is a problem specific to Internet Explorer. See https://github.com/tc39/ecma262/issues/1440 for (const engine in engines) { if (engine as Engine !== 'ES' && engine as Engine !== 'IE') { js.TypeofExoticObjectIsObject[engine as Engine] = { 0: { force: true } } } } // This is a problem specific to JavaScriptCore. Some examples of when the // problematic case happens (checked in Safari 12.1): // // ❱ x(function(y=-1){}.z=2) // SyntaxError: Left hand side of operator '=' must be a reference. // // ❱ x(class{f(y=-1){}}.z=2) // SyntaxError: Left hand side of operator '=' must be a reference. // // Some examples of cases that aren't problematic (checked in Safari 12.1): // // // Adding parentheses makes it ok // x((function(y=-1){}).z=2) // x((class{f(y=-1){}}).z=2) // // // Not using a unary operator in the default argument makes it ok // x(function(y=1){}.z=2) // x(class{f(y=1){}}.z=2) // // // Methods in object literals are not affected // x({f(y=-1){}}.z=2) // // We don't attempt to reverse-engineer the specific conditions that cause JSC // to exhibit the bug. Instead we just always wrap function and class literals // when they are nested inside of a property access. This workaround is overly // conservative but is the same thing that UglifyJS does to handle this case. // // See https://github.com/mishoo/UglifyJS/pull/2056 and https://github.com/evanw/esbuild/issues/3072 for (const engine in engines) { if (engine as Engine !== 'Safari') { js.FunctionOrClassPropertyAccess[engine as Engine] = { 0: { force: true } } } else { // These bugs are known to be fixed in Safari 16.3+ js.FunctionOrClassPropertyAccess.Safari = { '16.3': { force: true } } } } // This is a special case. Node added support for it to both v12.20+ and v13.2+ // so the range is inconveniently discontiguous. Sources: // // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import // - https://github.com/nodejs/node/pull/35950 // - https://github.com/nodejs/node/pull/31974 // js.DynamicImport.Node = { '12.20': { force: true }, '13': { force: false }, '13.2': { force: true }, } // Manually copied from https://nodejs.org/api/esm.html#node-imports js.NodeColonPrefixImport.Node = { '12.20': { force: true }, '13': { force: false }, '14.13.1': { force: true }, } js.NodeColonPrefixRequire.Node = { '14.18': { force: true }, '15': { force: false }, '16': { force: true }, } js.NodeColonPrefixImport.ES = { 0: { force: true } } js.NodeColonPrefixRequire.ES = { 0: { force: true } } // Arbitrary Module Namespace Names { // From https://github.com/tc39/ecma262/pull/2154#issuecomment-825201030 js.ArbitraryModuleNamespaceNames.Chrome = { 90: { force: true } } js.ArbitraryModuleNamespaceNames.Node = { 16: { force: true } } // From https://bugzilla.mozilla.org/show_bug.cgi?id=1670044 js.ArbitraryModuleNamespaceNames.Firefox = { 87: { force: true } } // From https://developer.apple.com/documentation/safari-release-notes/safari-14_1-release-notes js.ArbitraryModuleNamespaceNames.Safari = { '14.1': { force: true } } js.ArbitraryModuleNamespaceNames.IOS = { '14.5': { force: true } } } // Import assertions (note: these were removed from the JavaScript specification and never standardized) { js.ImportAssertions.Node = { // From https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V16.md#16.14.0 '16.14': { force: true }, // Manually tested using a binary from https://nodejs.org/en/download/prebuilt-binaries '22': { force: false }, } // MDN data is wrong here: https://bugs.webkit.org/show_bug.cgi?id=251600 delete js.ImportAssertions.IOS delete js.ImportAssertions.Safari } // Import attributes (the replacement for import assertions) { // Manually tested using binaries from https://nodejs.org/en/download/prebuilt-binaries js.ImportAttributes.Node = { '18.20': { force: true }, '19': { force: false }, '20.10': { force: true }, } } // MDN data is wrong here: https://www.chromestatus.com/feature/6482797915013120 js.ClassStaticBlocks.Chrome = { 91: { force: true } } // WebKit has now been marked as not supporting top-level await because it // turns out they have a pretty severe bug: Importing a module that uses // top-level await twice causes the second import to fail. For more info see: // https://bugs.webkit.org/show_bug.cgi?id=242740 // // However, we're going to override this to say it's still supported because: // - It always fails loudly when this happens (the import fails) // - People would otherwise be blocked from using this feature at all in // WebKit-based browsers // - From what I understand, most use of top-level await is in the entry-point // module, where this isn't an issue // - When you bundle your code, nested modules become entry points so this // issue also wouldn't come up // - The top-level await implementation in esbuild isn't even correct for // nested modules as they become linearized in the bundle (like Rollup, but // not like Webpack) js.TopLevelAwait.IOS = { 15: { force: true } } js.TopLevelAwait.Safari = { 15: { force: true } } // MDN data is incomplete here: https://github.com/mdn/browser-compat-data/issues/28228 js.FromBase64.Node = { 25: { force: true } } const [jsVersionRanges, jsWhyNot] = supportMapToVersionRanges(js) generateTableForJS(jsVersionRanges, jsWhyNot) }) const css: SupportMap = {} as SupportMap const cssPrefix: CSSPrefixMap = {} for (const feature in cssFeatures) css[feature as CSSFeature] = {} mergeSupportMaps(css, caniuse.css) mergeSupportMaps(css, mdn.css) mergePrefixMaps(cssPrefix, caniuse.cssPrefix) mergePrefixMaps(cssPrefix, mdn.cssPrefix) // MDN data is wrong here, Firefox still had gradient interpolation rendering // bugs until version 137: https://bugzilla.mozilla.org/show_bug.cgi?id=1904106 css.GradientInterpolation.Firefox = { 137: { force: true } } const [cssVersionRanges] = supportMapToVersionRanges(css) generateTableForCSS(cssVersionRanges, cssPrefix)