blob: 1a009bc2063966d1a00cbb96a85d5da9506cce69 [file] [log] [blame] [edit]
/* 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 {
BrowserName,
Identifier,
SimpleSupportStatement,
} from '../types/types.js';
import { InternalSupportStatement } from '../types/index.js';
import dataFolders from '../scripts/lib/data-folders.js';
import bcd from '../index.js';
interface StatusFilters {
deprecated: boolean | undefined;
standard_track: boolean | undefined;
experimental: boolean | undefined;
}
/**
* Traverse all of the features within a specified object and find all features that have one of the specified values
* @param obj The compat data to traverse through
* @param browsers The browsers to test for
* @param values The values to test for
* @param depth The depth to traverse
* @param tag The tag to filter results with
* @param identifier The identifier of the current object
* @param status Whether to filter by status flags
* @yields {string} The feature identifier
*/
export function* iterateFeatures(
obj: Identifier,
browsers: BrowserName[],
values: string[],
depth: number,
tag: string,
identifier: string,
status: StatusFilters | null = null,
): IterableIterator<string> {
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) {
let browserData:
| SimpleSupportStatement
| SimpleSupportStatement[]
| undefined = 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 (
(browserData[range] as InternalSupportStatement) === '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 obj The compat data to traverse through
* @param browsers The browsers to traverse for
* @param values The version values to traverse for
* @param depth The depth to traverse
* @param tag The tag to filter results with
* @param identifier The identifier of the current object
* @param status Whether to filter by status flags
* @returns An array of the features
*/
const traverseFeatures = (
obj: Identifier,
browsers: BrowserName[],
values: string[],
depth: number,
tag: string,
identifier: string,
status: StatusFilters,
): string[] => {
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 folders The folders to traverse
* @param browsers The browsers to traverse for
* @param values The version values to traverse for
* @param depth The depth to traverse
* @param tag The tag to filter results with
* @param status Whether to filter by status flags
* @returns The list of features
*/
const main = (
folders = dataFolders.concat('webextensions'),
browsers: BrowserName[] = Object.keys(bcd.browsers).filter(
(b) => bcd.browsers[b].type !== 'server',
) as BrowserName[],
values = [],
depth = 100,
tag = '',
status = {} as StatusFilters,
): string[] => {
const features: string[] = [];
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 }: { argv } = yargs(hideBin(process.argv)).command(
'$0 [folder...]',
'Print feature names in the folder (and optionally filter features to specific browser or version values)',
(yargs) => {
yargs
.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: 'array',
nargs: 1,
default: Object.keys(bcd.browsers).filter(
(b) => bcd.browsers[b].type !== 'server',
),
})
.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('status.deprecated', {
alias: 'x',
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('status.standard_track', {
alias: 's',
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('status.experimental', {
alias: 'e',
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.',
);
},
);
const features = main(
argv.folder,
argv.browser,
argv.filter,
argv.depth,
argv.tag,
argv.status,
);
console.log(features.join('\n'));
if (argv.showCount) {
console.log(features.length);
}
}
export default main;