From 7b1be75cd50e4cc3408ebe03cfe263210fae1806 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 11 Sep 2026 14:39:40 +0200 Subject: [PATCH] feat(scripts): add flag inventory --- package.json | 1 + scripts/flag-inventory.js | 66 +++++++++++++++++++++++++ scripts/flag-inventory.test.js | 89 ++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 scripts/flag-inventory.js create mode 100644 scripts/flag-inventory.test.js diff --git a/package.json b/package.json index 80625c5d135603..b33132c5946dd0 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "lint:fix": "node lint/fix.js", "fix": "npm run format:fix && npm run lint:fix", "stats": "node scripts/statistics.js", + "flag-inventory": "node scripts/flag-inventory.js", "build": "node scripts/build/index.js", "gentypes": "node scripts/generate-types.js", "release": "node scripts/release/index.js", diff --git a/scripts/flag-inventory.js b/scripts/flag-inventory.js new file mode 100644 index 00000000000000..0d93cc4b6ffef8 --- /dev/null +++ b/scripts/flag-inventory.js @@ -0,0 +1,66 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +import esMain from 'es-main'; + +import walk from '../utils/walk.js'; + +/** @import {InternalDataType} from '../types/index.js' */ + +/** + * Compare map entries by key using locale-independent alphabetical order. + * @template T + * @param {[string, T]} a First entry + * @param {[string, T]} b Second entry + * @returns {number} Sort order + */ +const compareKeys = ([a], [b]) => (a < b ? -1 : a > b ? 1 : 0); + +/** + * List explicit flag occurrences by type, browser, and name, alphabetically. + * Each flags entry counts once, regardless of value or support version. + * @param {InternalDataType} [data] Source compatibility data + * @returns {string} The indented flag inventory + */ +const flagInventory = (data) => { + /** @type {Map>>} */ + const inventory = new Map(); + + for (const { compat } of walk(undefined, data)) { + for (const [browser, support] of Object.entries(compat.support)) { + if (support === 'mirror') { + continue; + } + for (const statement of Array.isArray(support) ? support : [support]) { + for (const { type, name } of statement.flags ?? []) { + const browsers = inventory.get(type) ?? new Map(); + inventory.set(type, browsers); + const flags = browsers.get(browser) ?? new Map(); + browsers.set(browser, flags); + flags.set(name, (flags.get(name) ?? 0) + 1); + } + } + } + } + + const lines = []; + for (const [type, browsers] of [...inventory].sort(compareKeys)) { + lines.push(type); + for (const [browser, flags] of [...browsers].sort(compareKeys)) { + lines.push(` ${browser}`); + for (const [name, count] of [...flags].sort(compareKeys)) { + lines.push(` ${name}: ${count}`); + } + } + } + return lines.join('\n'); +}; + +if (esMain(import.meta)) { + const output = flagInventory(); + if (output) { + console.log(output); + } +} + +export default flagInventory; diff --git a/scripts/flag-inventory.test.js b/scripts/flag-inventory.test.js new file mode 100644 index 00000000000000..77ad15b4265c9a --- /dev/null +++ b/scripts/flag-inventory.test.js @@ -0,0 +1,89 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import flagInventory from './flag-inventory.js'; + +describe('flagInventory', () => { + it('counts all explicit occurrences and sorts each grouping', () => { + // The recursive InternalIdentifier index signature cannot type this fixture. + // eslint-disable-next-line jsdoc/reject-any-type + /** @type {*} */ + const data = { + api: { + Example: { + __compat: { + support: { + firefox: [ + { + version_added: '2', + flags: [ + { type: 'runtime_flag', name: 'z-flag' }, + { type: 'preference', name: 'z-pref' }, + { + type: 'preference', + name: 'a-pref', + value_to_set: 'true', + }, + ], + }, + { + version_added: '1', + version_removed: '2', + flags: [ + { + type: 'preference', + name: 'a-pref', + value_to_set: 'false', + }, + ], + }, + ], + firefox_android: 'mirror', + chrome: { + version_added: '1', + flags: [{ type: 'preference', name: 'a-pref' }], + }, + safari: { version_added: false }, + }, + }, + nested: { + __compat: { + support: { + firefox: { + version_added: '1', + flags: [ + { type: 'preference', name: 'a-pref' }, + { type: 'runtime_flag', name: 'a-pref' }, + ], + }, + }, + }, + }, + }, + }, + }; + + assert.equal( + flagInventory(data), + [ + 'preference', + ' chrome', + ' a-pref: 1', + ' firefox', + ' a-pref: 3', + ' z-pref: 1', + 'runtime_flag', + ' firefox', + ' a-pref: 1', + ' z-flag: 1', + ].join('\n'), + ); + }); + + it('returns empty output when there are no flags', () => { + assert.equal(flagInventory({}), ''); + }); +});