eslint-plugin-compat
Version:
Lint browser compatibility of API used
199 lines (197 loc) • 8.17 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.lintCallExpression = lintCallExpression;
exports.lintNewExpression = lintNewExpression;
exports.lintExpressionStatement = lintExpressionStatement;
exports.lintMemberExpression = lintMemberExpression;
exports.reverseTargetMappings = reverseTargetMappings;
exports.determineTargetsFromConfig = determineTargetsFromConfig;
exports.parseBrowsersListVersion = parseBrowsersListVersion;
/* eslint no-nested-ternary: off */
const browserslist_1 = __importDefault(require("browserslist"));
/*
3) Figures out which browsers user is targeting
- Uses browserslist config and/or targets defined eslint config to discover this
- For every API ecnountered during traversal, gets compat record for that
- Protochain (e.g. 'document.querySelector')
- All of the rules have compatibility info attached to them
- Each API is given to versioning.ts with compatibility info
*/
function isInsideIfStatement(node, sourceCode, context) {
const ancestors = "getAncestors" in sourceCode
? // @ts-expect-error Fits
sourceCode?.getAncestors?.(node)
: context.getAncestors();
return ancestors?.some((ancestor) => {
return ancestor.type === "IfStatement";
});
}
function checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node) {
if (!isInsideIfStatement(node, sourceCode, context)) {
handleFailingRule(failingRule, node);
}
}
function lintCallExpression(context, handleFailingRule, rules, sourceCode, node) {
if (!node.callee)
return;
const calleeName = node.callee.name;
const failingRule = rules.find((rule) => rule.object === calleeName);
if (failingRule)
checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node);
}
function lintNewExpression(context, handleFailingRule, rules, sourceCode, node) {
if (!node.callee)
return;
const calleeName = node.callee.name;
const failingRule = rules.find((rule) => rule.object === calleeName);
if (failingRule)
checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node);
}
function lintExpressionStatement(context, handleFailingRule, rules, sourceCode, node) {
if (!node?.expression?.name)
return;
const failingRule = rules.find((rule) => rule.object === node?.expression?.name);
if (failingRule)
checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node);
}
function isStringLiteral(node) {
return node.type === "Literal" && typeof node.value === "string";
}
function protoChainFromMemberExpression(node) {
if (!node.object)
return [node.name];
const protoChain = (() => {
if (node.object.type === "NewExpression" ||
node.object.type === "CallExpression") {
return protoChainFromMemberExpression(node.object.callee);
}
else if (node.object.type === "ArrayExpression") {
return ["Array"];
}
else if (isStringLiteral(node.object)) {
return ["String"];
}
else {
return protoChainFromMemberExpression(node.object);
}
})();
return [...protoChain, node.property.name];
}
function lintMemberExpression(context, handleFailingRule, rules, sourceCode, node) {
if (!node.object || !node.property)
return;
if (!node.object.name ||
node.object.name === "window" ||
node.object.name === "globalThis") {
const rawProtoChain = protoChainFromMemberExpression(node);
const [firstObj] = rawProtoChain;
const protoChain = firstObj === "window" || firstObj === "globalThis"
? rawProtoChain.slice(1)
: rawProtoChain;
const protoChainId = protoChain.join(".");
const failingRule = rules.find((rule) => rule.protoChainId === protoChainId);
if (failingRule) {
checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node);
}
}
else {
const objectName = node.object.name;
const propertyName = node.property.name;
const failingRule = rules.find((rule) => rule.object === objectName &&
(rule.property == null || rule.property === propertyName));
if (failingRule)
checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node);
}
}
function reverseTargetMappings(targetMappings) {
const reversedEntries = Object.entries(targetMappings).map((entry) => entry.reverse());
return Object.fromEntries(reversedEntries);
}
/**
* Determine the targets based on the browserslist config object
* Get the targets from the eslint config and merge them with targets in browserslist config
* Eslint target config will be deprecated in 4.0.0
*
* @param configPath - The file or a directory path to look for the browserslist config file
*/
function determineTargetsFromConfig(configPath, config, browserslistOptsFromConfig) {
const browserslistOpts = { path: configPath, ...browserslistOptsFromConfig };
const eslintTargets = (() => {
// Get targets from eslint settings
if (Array.isArray(config) || typeof config === "string") {
return (0, browserslist_1.default)(config, browserslistOpts);
}
if (config && typeof config === "object") {
return (0, browserslist_1.default)([...(config.production || []), ...(config.development || [])], browserslistOpts);
}
return [];
})();
if (browserslist_1.default.findConfig(configPath)) {
// If targets are defined in ESLint and browerslist configs, merge the targets together
if (eslintTargets.length) {
const browserslistTargets = (0, browserslist_1.default)(undefined, browserslistOpts);
return Array.from(new Set(eslintTargets.concat(browserslistTargets)));
}
}
else if (eslintTargets.length) {
return eslintTargets;
}
// Get targets fron browserslist configs
return (0, browserslist_1.default)(undefined, browserslistOpts);
}
/**
* Parses the versions that are given by browserslist. They're
*
* ```ts
* parseBrowsersListVersion(['chrome 50'])
*
* {
* target: 'chrome',
* parsedVersion: 50,
* version: '50'
* }
* ```
* @param targetslist - List of targest from browserslist api
* @returns - The lowest version version of each target
*/
function parseBrowsersListVersion(targetslist) {
return (
// Sort the targets by target name and then version number in ascending order
targetslist
.map((e) => {
const [target, version] = e.split(" ");
const parsedVersion = (() => {
if (typeof version === "number")
return version;
if (version === "all")
return 0;
return version.includes("-")
? parseFloat(version.split("-")[0])
: parseFloat(version);
})();
return {
target,
version,
parsedVersion,
};
}) // Sort the targets by target name and then version number in descending order
// ex. [a@3, b@3, a@1] => [a@3, a@1, b@3]
.sort((a, b) => {
if (b.target === a.target) {
// If any version === 'all', return 0. The only version of op_mini is 'all'
// Otherwise, compare the versions
return typeof b.parsedVersion === "string" ||
typeof a.parsedVersion === "string"
? 0
: b.parsedVersion - a.parsedVersion;
}
return b.target > a.target ? 1 : -1;
}) // First last target always has the latest version
.filter((e, i, items) =>
// Check if the current target is the last of its kind.
// If it is, then it's the most recent version.
i + 1 === items.length || e.target !== items[i + 1].target));
}
;