blob: 231684e8a376bfd2914760b3682bcdf3d05764ed [file] [log] [blame]
/* This file is a part of @mdn/browser-compat-data
* See LICENSE file for more information. */
import esMain from 'es-main';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import dataFolders from '../scripts/lib/data-folders.js';
import bcd from '../index.js';
/** @import {BrowserName, Identifier, SimpleSupportStatement} from '../types/types.js' */
/** @import {InternalSupportStatement} from '../types/index.js' */
/**
* @typedef {object} StatusFilters
* @property {boolean | undefined} deprecated
* @property {boolean | undefined} standard_track
* @property {boolean | undefined} experimental
*/
/**
* Traverse all of the features within a specified object and find all features that have one of the specified values
* @param {Identifier} obj The compat data to traverse through
* @param {BrowserName[]} browsers The browsers to test for
* @param {string[]} values The values to test for
* @param {number} depth The depth to traverse
* @param {string} tag The tag to filter results with
* @param {string} identifier The identifier of the current object
* @param {StatusFilters | null} [status] Whether to filter by status flags
* @yields {string} The feature identifier
* @returns {IterableIterator<string>}
*/
export function* iterateFeatures(
obj,
browsers,
values,
depth,
tag,
identifier,
status = null,
) {
const { deprecated, standard_track, experimental } = status ?? {};
depth--;
if (depth >= 0) {
for (const i in obj) {
if (!!obj[i] && typeof obj[i] == 'object' && i !== '__compat') {
if (obj[i].__compat) {
if (typeof deprecated === 'boolean') {
if (deprecated !== obj[i].__compat.status?.deprecated) {
continue;
}
}
if (typeof standard_track === 'boolean') {
if (standard_track !== obj[i].__compat.status?.standard_track) {
continue;
}
}
if (typeof experimental === 'boolean') {
if (experimental !== obj[i].__compat.status?.experimental) {
continue;
}
}
if (tag) {
const tags = obj[i].__compat?.tags;
if ((tags && tags.includes(tag)) || (!tags && tag == 'false')) {
yield `${identifier}${i}`;
}
} else {
const comp = obj[i].__compat?.support;
if (!comp) {
continue;
}
for (const browser of browsers) {
/** @type {SimpleSupportStatement | SimpleSupportStatement[] | undefined} */
let browserData = comp[browser];
if (!browserData) {
if (values.length == 0 || values.includes('null')) {
// Web extensions only allows specific browsers
if (
!(
identifier.startsWith('webextensions.') &&
bcd.browsers[browser].accepts_webextensions
)
) {
continue;
}
yield `${identifier}${i}`;
}
continue;
}
if (!Array.isArray(browserData)) {
browserData = [browserData];
}
for (const range in browserData) {
if (
/** @type {InternalSupportStatement} */ (
browserData[range]
) === 'mirror'
) {
if (values.includes('mirror')) {
yield `${identifier}${i}`;
}
} else if (values.includes('nonmirror')) {
// If checking for non-mirrored data and it's not mirrored
yield `${identifier}${i}`;
} else if (browserData[range] === undefined) {
if (values.length == 0 || values.includes('null')) {
yield `${identifier}${i}`;
}
} else if (values.includes('≤') || values.includes('ranged')) {
if (
String(browserData[range].version_added).startsWith('≤') ||
String(browserData[range].version_removed).startsWith('≤')
) {
yield `${identifier}${i}`;
}
} else if (
values.length == 0 ||
values.includes(String(browserData[range].version_added)) ||
values.includes(String(browserData[range].version_removed))
) {
let f = `${identifier}${i}`;
if (browserData[range].prefix) {
f += ` (${browserData[range].prefix} prefix)`;
}
if (browserData[range].alternative_name) {
f += ` (as ${browserData[range].alternative_name})`;
}
yield f;
}
}
}
}
}
yield* iterateFeatures(
obj[i],
browsers,
values,
depth,
tag,
identifier + i + '.',
status,
);
}
}
}
}
/**
* Traverse all of the features within a specified object and find all features that have one of the specified values
* @param {Identifier} obj The compat data to traverse through
* @param {BrowserName[]} browsers The browsers to traverse for
* @param {string[]} values The version values to traverse for
* @param {number} depth The depth to traverse
* @param {string} tag The tag to filter results with
* @param {string} identifier The identifier of the current object
* @param {StatusFilters} status Whether to filter by status flags
* @returns {string[]} An array of the features
*/
const traverseFeatures = (
obj,
browsers,
values,
depth,
tag,
identifier,
status,
) => {
const features = Array.from(
iterateFeatures(obj, browsers, values, depth, tag, identifier, status),
);
return features.filter((item, pos) => features.indexOf(item) == pos);
};
/**
* Traverse the features within BCD
* @param {string[]} [folders] The folders to traverse
* @param {BrowserName[]} [browsers] The browsers to traverse for
* @param {string[]} [values] The version values to traverse for
* @param {number} [depth] The depth to traverse
* @param {string} [tag] The tag to filter results with
* @param {StatusFilters} [status] Whether to filter by status flags
* @returns {string[]} The list of features
*/
const main = (
folders = dataFolders.concat('webextensions'),
browsers = /** @type {BrowserName[]} */ (
Object.keys(bcd.browsers).filter((b) => bcd.browsers[b].type !== 'server')
),
values = [],
depth = 100,
tag = '',
status = /** @type {StatusFilters} */ ({}),
) => {
/** @type {string[]} */
const features = [];
for (const folder in folders) {
features.push(
...traverseFeatures(
bcd[folders[folder]],
browsers,
values,
depth,
tag,
folders[folder] + '.',
status,
),
);
}
return features;
};
if (esMain(import.meta)) {
const argv = yargs(hideBin(process.argv))
.command(
'$0 [folder...]',
'Print feature names in the folder (and optionally filter features to specific browser or version values)',
)
.positional('folder', {
describe: 'The folder(s) to traverse',
type: 'string',
array: true,
default: Object.keys(bcd).filter((k) => k !== 'browsers'),
})
.option('browser', {
alias: 'b',
describe: 'Filter by a browser. May repeat.',
type: 'string',
nargs: 1,
default: Object.keys(bcd.browsers).filter(
(b) => bcd.browsers[b].type !== 'server',
),
/**
*
* @param value
* @returns {BrowserName[]}
*/
coerce: (value) =>
/** @type {BrowserName[]} */ (
(Array.isArray(value) ? value : [value]).filter((value) =>
Object.keys(bcd.browsers).some((browser) => browser === value),
)
),
})
.option('filter', {
alias: 'f',
describe:
'Filter by version value. May repeat. Set to "≤" or "ranged" for ranged values (ex. ≤58), "mirror" for mirrored entries, and "nonmirror" for non-mirrored entries.',
type: 'array',
string: true,
nargs: 1,
default: [],
})
.option('tag', {
alias: 't',
describe:
'Filter by tag value. Set to `false` to search for features with no tags.',
type: 'string',
nargs: 1,
default: '',
})
.option('depth', {
alias: 'd',
describe:
'Depth of features to traverse (ex. "2" will capture "api.CSSStyleSheet.insertRule" but not "api.CSSStyleSheet.insertRule.optional_index")',
type: 'number',
nargs: 1,
default: 10,
})
.option('show-count', {
alias: 'c',
describe: 'Show the count of features traversed at the end',
type: 'boolean',
default: process.stdout.isTTY,
})
.option('deprecated', {
alias: ['x', 'status.deprecated'],
describe:
'Filter features by deprecation status. Set to `true` to only show deprecated features or `false` to only show non-deprecated features.',
type: 'boolean',
default: undefined,
})
.option('standard-track', {
alias: ['s', 'status.standard_track'],
describe:
'Filter features by standard_track status. Set to `true` to only show standards track features or `false` to only show non-standards track features.',
type: 'boolean',
default: undefined,
})
.option('experimental', {
alias: ['e', 'status.experimental'],
describe:
'Filter features by experimental status. Set to `true` to only show experimental features or `false` to only show non-experimental features.',
type: 'boolean',
default: undefined,
})
.example(
'npm run traverse -- -b webview_android -f ≤37',
'Find all features marked as ≤37 for WebView',
)
.example(
'npm run traverse -- -b firefox -f 10',
'Find all features marked as supported since Firefox 10',
)
.example(
'npm run traverse -- -b samsunginternet_android -f mirror',
'Find all features in Samsung Internet that mirror data from Chrome Android',
)
.example(
'npm run traverse -- -t web-features:idle-detection',
'Find all features tagged with web-features:idle-detection.',
)
.example('npm run traverse -- -t false', 'Find all features with no tags.')
.example(
'npm run traverse -- --status.deprecated',
'Find all features that are deprecated.',
)
.example(
'npm run traverse -- --no-status.deprecated',
'Omit all features that are deprecated.',
)
.example(
'npm run traverse -- --status.standard_track',
'Find all features that are on the standard track.',
)
.example(
'npm run traverse -- --status.experimental',
'Find all features that are experimental.',
)
.parseSync();
const { deprecated, standardTrack: standard_track, experimental } = argv;
const features = main(
argv.folder,
argv.browser,
argv.filter,
argv.depth,
argv.tag,
{ deprecated, standard_track, experimental },
);
console.log(features.join('\n'));
if (argv.showCount) {
console.log(features.length);
}
}
export default main;