├── .gitignore ├── package.json ├── yarn.lock └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-features-evergreen", 3 | "main": "index.js", 4 | "scripts": { 5 | "update": "node main.js" 6 | }, 7 | "license": "MIT", 8 | "dependencies": { 9 | "@mdn/browser-compat-data": "5.6.6", 10 | "caniuse-db": "1.0.30001668" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@mdn/browser-compat-data@5.6.6": 6 | version "5.6.6" 7 | resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.6.6.tgz#34d0468b07e7841cca2fc40dc7fc146b0bcf09ee" 8 | integrity sha512-Ar810M/WlJUpUt0uDxeUO8+UJ1fV4dbyilqYzOhPcBfjkgV454vs9S77IMcVcnPqu7o12tPGd1S1Wj9nDnn21A== 9 | 10 | caniuse-db@1.0.30001668: 11 | version "1.0.30001668" 12 | resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001668.tgz#9c4cfd5a53456337398dd7f92aa0560bb289ed1c" 13 | integrity sha512-Qlhxa6XRgm+CxH7s9vR+q60upI4qOVhLPJJ/SL6qdp29U9XL9+8psdyb6MqrKr6U6pWdkLZI2MzOPoD9Ut5SPQ== 14 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const browserCompat = require('@mdn/browser-compat-data'); 3 | 4 | let output = ''; 5 | const print = (...args) => output += '\n' + args.join(' '); 6 | 7 | const browsersToConsider = [ 8 | 'chrome', 9 | 'edge', 10 | 'firefox', 11 | 'safari_ios', 12 | 'safari', 13 | ]; 14 | 15 | const releasesCache = {}; 16 | // Returns newest first. 17 | function sortBrowserReleasesByReleaseDate(browser) { 18 | if (releasesCache[browser]) return releasesCache[browser]; 19 | 20 | const releases = browserCompat.browsers[browser].releases; 21 | const asArray = Object.entries(releases).map(entry => ({ version: entry[0], details: entry[1] })); 22 | releasesCache[browser] = asArray.sort((a, b) => new Date(b.details.release_date) - new Date(a.details.release_date)); 23 | return asArray; 24 | } 25 | 26 | /** 27 | * Stable. 28 | * @param {Date} month 29 | */ 30 | function findBrowsersReleasedInMonth(month) { 31 | const result = []; 32 | 33 | for (const browser of Object.keys(browserCompat.browsers)) { 34 | if (!browsersToConsider.includes(browser)) continue; 35 | 36 | // To simplify, only consider the latest version from each browser. 37 | const releaseEntry = sortBrowserReleasesByReleaseDate(browser).find(({ version, details }) => { 38 | if (!details.release_date) return false; 39 | 40 | const releaseDate = new Date(details.release_date); 41 | return releaseDate.getYear() === month.getYear() && releaseDate.getMonth() === month.getMonth(); 42 | }); 43 | if (!releaseEntry) continue; 44 | 45 | result.push({ 46 | browser, 47 | version: releaseEntry.version, 48 | }); 49 | } 50 | 51 | return result; 52 | } 53 | 54 | /** 55 | * @param {Date} month 56 | */ 57 | function findStableBrowsersInMonth(month) { 58 | const result = []; 59 | 60 | for (const browser of Object.keys(browserCompat.browsers)) { 61 | if (!browsersToConsider.includes(browser)) continue; 62 | 63 | const releaseEntries = sortBrowserReleasesByReleaseDate(browser).filter(({ details }) => { 64 | if (!details.release_date) return false; 65 | 66 | const releaseDate = new Date(details.release_date); 67 | return month.getYear() > releaseDate.getYear() || (month.getYear() === releaseDate.getYear() && month.getMonth() > releaseDate.getMonth()); 68 | }); 69 | const releaseEntry = releaseEntries[0]; 70 | if (!releaseEntry) continue; 71 | 72 | result.push({ 73 | browser, 74 | version: releaseEntry.version, 75 | }); 76 | } 77 | 78 | return result; 79 | } 80 | 81 | function findBrowserVersionReleaseIndex(browser, version) { 82 | const releases = sortBrowserReleasesByReleaseDate(browser); 83 | return releases.findIndex(release => release.version === version); 84 | } 85 | 86 | const compatData = getCompatData(); 87 | function getCompatData() { 88 | let compats = []; 89 | 90 | function findCompats(obj, path = []) { 91 | if (!(typeof obj === 'object' && obj)) return; 92 | 93 | for (const [key, child] of Object.entries(obj)) { 94 | if (!child) continue; 95 | if (child.__compat) { 96 | compats.push({ 97 | id: [...path, key].join('.'), 98 | compat: child.__compat, 99 | }); 100 | } 101 | findCompats(child, [...path, key]); 102 | } 103 | } 104 | 105 | const { browser, ...rootCompatObj } = browserCompat; 106 | findCompats(rootCompatObj); 107 | return compats; 108 | } 109 | 110 | function getFeatureSetForBrowserVersion({ browser, version }) { 111 | const features = []; 112 | 113 | for (const feature of compatData) { 114 | if (!feature.compat.support[browser]) continue; 115 | 116 | const supportStatement = Array.isArray(feature.compat.support[browser]) ? 117 | feature.compat.support[browser][0] : 118 | feature.compat.support[browser]; 119 | if (!supportStatement.version_added) continue; 120 | if (supportStatement.version_removed) continue; 121 | if (supportStatement.flags) continue; 122 | 123 | const isSupported = supportStatement.version_added === true || 124 | findBrowserVersionReleaseIndex(browser, version) <= findBrowserVersionReleaseIndex(browser, supportStatement.version_added); 125 | if (isSupported) { 126 | if (supportStatement.flags) print(feature.id, supportStatement); 127 | features.push(feature.id); 128 | } 129 | } 130 | 131 | return new Set(features); 132 | } 133 | 134 | function getFeatureSetForBrowserVersions(browserVerisons) { 135 | return setIntersections(...browserVerisons.map(getFeatureSetForBrowserVersion)); 136 | } 137 | 138 | function setIntersections(...sets) { 139 | const result = new Set(); 140 | const [firstSet, ...rest] = sets; 141 | 142 | for (const item of firstSet) { 143 | if (rest.every(set => set.has(item))) { 144 | result.add(item); 145 | } 146 | } 147 | 148 | return result; 149 | } 150 | 151 | function setDifference(a, b) { 152 | const result = new Set(); 153 | 154 | for (const item of a) { 155 | if (b.has(item)) continue; 156 | result.add(item); 157 | } 158 | for (const item of b) { 159 | if (a.has(item)) continue; 160 | result.add(item); 161 | } 162 | 163 | return result; 164 | } 165 | 166 | function yearInReview(year) { 167 | const threeMonthsFromToday = new Date(new Date() - -1000 * 60 * 60 * 24 * 30 * 3); 168 | print(`\n# ${year}\n\n`); 169 | for (let i = 12; i >= 1; i--) { 170 | const lastMonth = new Date(i === 1 ? `${year - 1}/12/01` : `${year}/${i - 1}/01`); 171 | const thisMonth = new Date(`${year}/${i}/01`); 172 | if (thisMonth > threeMonthsFromToday) continue; 173 | const newReleases = findBrowsersReleasedInMonth(new Date(thisMonth)); 174 | const lastMonthStableBrowsers = findStableBrowsersInMonth(lastMonth); 175 | const thisMonthStableBrowsers = findStableBrowsersInMonth(thisMonth); 176 | const lastMonthFeatureSet = getFeatureSetForBrowserVersions(lastMonthStableBrowsers); 177 | const thisMonthFeatureSet = getFeatureSetForBrowserVersions(thisMonthStableBrowsers); 178 | const difference = setDifference(lastMonthFeatureSet, thisMonthFeatureSet); 179 | 180 | print(`\n## ${thisMonth.toLocaleString('en-us', { month: 'short', year: 'numeric' })}`); 181 | newReleases.length && print(`### Browsers released:\n`, 182 | ' - ' + newReleases.map(r => JSON.stringify(r).replace(/"/g, `'`)).join('\n - ')); 183 | if (difference.size) { 184 | print(`### These Features became stable across all major browsers:`); 185 | 186 | for (const feature of difference) { 187 | // Skip sub-features if their parent feature (eg `api.OffscreenCanvasRenderingContext2D`) shipped. 188 | if (difference.has(feature.substr(0, feature.lastIndexOf('.')))) continue; 189 | 190 | const mdn_url = compatData.find(f => f.id === feature)?.compat?.mdn_url; 191 | print(mdn_url ? ` - [\`${feature}\`](${mdn_url})` : ` - \`${feature}\``); 192 | } 193 | } 194 | } 195 | print('\n'); 196 | } 197 | 198 | for (let year = new Date().getFullYear(); year >= 2018; year--) { 199 | yearInReview(year); 200 | } 201 | fs.writeFileSync('./readme.md', output, 'utf-8'); 202 | 203 | 204 | 205 | // console.log(getFeatureSetForBrowserVersion({ browser: 'safari', version: '15.1' }).has('api.AudioWorkletNode')); 206 | // console.log(browserCompat.api.AudioWorkletNode.__compat.support); 207 | --------------------------------------------------------------------------------