es-check 9.7.1 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ const { ES16_FEATURES } = require("./16");
12
12
  const { ES17_FEATURES } = require("./17");
13
13
  const { ES_GLOBAL_MIN_VERSION, ES_GLOBALS_SOURCE } = require("./globals");
14
14
  const { POLYFILL_PATTERNS, IMPORT_PATTERNS, FEATURE_TO_POLYFILL_MAP } = require("./polyfills");
15
+ const { FEATURE_BROWSER_SUPPORT } = require("./browserSupport");
15
16
 
16
17
  const ES_FEATURES = Object.assign(
17
18
  {},
@@ -36,4 +37,5 @@ module.exports = {
36
37
  POLYFILL_PATTERNS,
37
38
  IMPORT_PATTERNS,
38
39
  FEATURE_TO_POLYFILL_MAP,
40
+ FEATURE_BROWSER_SUPPORT,
39
41
  };
@@ -225,10 +225,28 @@ const VERSION_ORDER = [
225
225
 
226
226
  const LATEST_PARSER_VERSION = 2026;
227
227
 
228
+ const BROWSERSLIST_TO_BCD = {
229
+ and_chr: "chrome_android",
230
+ and_ff: "firefox_android",
231
+ android: "webview_android",
232
+ chrome: "chrome",
233
+ deno: "deno",
234
+ edge: "edge",
235
+ firefox: "firefox",
236
+ ie: "ie",
237
+ ios_saf: "safari_ios",
238
+ node: "nodejs",
239
+ op_mob: "opera_android",
240
+ opera: "opera",
241
+ safari: "safari",
242
+ samsung: "samsunginternet_android",
243
+ };
244
+
228
245
  module.exports = {
229
246
  ECMA_VERSION_MAP,
230
247
  ECMA_VERSION_TO_NUMBER,
231
248
  BROWSER_TO_ES_VERSION,
249
+ BROWSERSLIST_TO_BCD,
232
250
  JS_VERSIONS,
233
251
  VERSION_ORDER,
234
252
  LATEST_PARSER_VERSION,
@@ -7,6 +7,7 @@ const {
7
7
  const { ES_GLOBAL_MIN_VERSION } = require("./constants/es-features/globals");
8
8
  const { POLYFILLABLE_FEATURES, CORE_JS_POLYFILLABLE } = require("./constants/polyfillableFeatures");
9
9
  const { detectFeaturesFromAST } = require("./helpers/astDetector");
10
+ const { getUnsupportedBrowsers, formatFeatureWithBrowsers } = require("./helpers/browserSupport");
10
11
 
11
12
  function getPolyfillableFeatures(library) {
12
13
  if (!library) {
@@ -50,7 +51,7 @@ const detectPolyfills = (
50
51
  };
51
52
 
52
53
  const detectFeatures = (code, ecmaVersion, sourceType, ignoreList = new Set(), options = {}) => {
53
- const { checkForPolyfills, ast, ignorePolyfillable } = options;
54
+ const { checkForPolyfills, ast, ignorePolyfillable, targetBrowsers } = options;
54
55
 
55
56
  if (!ast) {
56
57
  const error = new Error("AST is required for feature detection");
@@ -65,17 +66,26 @@ const detectFeatures = (code, ecmaVersion, sourceType, ignoreList = new Set(), o
65
66
 
66
67
  const polyfillableSet = ignorePolyfillable ? getPolyfillableFeatures(ignorePolyfillable) : null;
67
68
 
69
+ const browserDetails = {};
70
+
68
71
  function isUnsupportedFeature(name, minVersion) {
69
72
  const wasFound = foundFeatures[name];
73
+ if (!wasFound) return false;
74
+
75
+ const unsupportedBrowsers = getUnsupportedBrowsers(name, targetBrowsers);
76
+ const lacksBrowserSupport = unsupportedBrowsers.length > 0;
70
77
  const requiresHigherVersion = minVersion > ecmaVersion;
78
+ const shouldReport = requiresHigherVersion || lacksBrowserSupport;
79
+ if (!shouldReport) return false;
80
+
71
81
  const isIgnored = ignoreList.has(name);
72
82
  const isPolyfilled = checkForPolyfills && polyfills.has(name);
73
83
  const isPolyfillableIgnored = polyfillableSet && polyfillableSet.has(name);
74
-
75
84
  const isAllowed = isIgnored || isPolyfilled || isPolyfillableIgnored;
76
- const shouldReport = wasFound && requiresHigherVersion;
77
- if (!shouldReport) return false;
78
- return !isAllowed;
85
+ if (isAllowed) return false;
86
+
87
+ if (lacksBrowserSupport) browserDetails[name] = unsupportedBrowsers;
88
+ return true;
79
89
  }
80
90
 
81
91
  const unsupportedStandardFeatures = Object.entries(ES_FEATURES)
@@ -90,12 +100,17 @@ const detectFeatures = (code, ecmaVersion, sourceType, ignoreList = new Set(), o
90
100
  const unsupportedFeatures = unsupportedStandardFeatures.concat(unsupportedGlobalFeatures);
91
101
 
92
102
  if (unsupportedFeatures.length > 0) {
93
- const error = new Error(
94
- `Unsupported features detected: ${unsupportedFeatures.join(", ")}. ` +
95
- `These require a higher ES version than ${ecmaVersion}.`,
96
- );
103
+ const hasBrowserDetails = Object.keys(browserDetails).length > 0;
104
+ const featureList = unsupportedFeatures
105
+ .map((name) => formatFeatureWithBrowsers(name, browserDetails[name]))
106
+ .join(", ");
107
+ const reason = hasBrowserDetails
108
+ ? `These require a higher ES version than ${ecmaVersion} or are not supported by the target browsers.`
109
+ : `These require a higher ES version than ${ecmaVersion}.`;
110
+ const error = new Error(`Unsupported features detected: ${featureList}. ${reason}`);
97
111
  error.type = "ES-Check";
98
112
  error.features = unsupportedFeatures;
113
+ error.browserDetails = browserDetails;
99
114
  error.ecmaVersion = ecmaVersion;
100
115
  throw error;
101
116
  }
@@ -103,6 +118,7 @@ const detectFeatures = (code, ecmaVersion, sourceType, ignoreList = new Set(), o
103
118
  return {
104
119
  foundFeatures,
105
120
  unsupportedFeatures,
121
+ browserDetails,
106
122
  };
107
123
  };
108
124
 
@@ -1609,16 +1609,49 @@ function getTypeofComparisonGuardBranch(comparison) {
1609
1609
 
1610
1610
  function getTypeofComparison(node) {
1611
1611
  if (node.type !== "BinaryExpression") return null;
1612
- if (!isTypeofComparisonOperator(node.operator)) return null;
1613
1612
 
1614
1613
  const leftComparison = getTypeofComparisonSide(node.left, node.right);
1615
1614
  if (leftComparison) {
1616
- return Object.assign({}, leftComparison, { operator: node.operator });
1615
+ return normalizeTypeofComparison(leftComparison, node.operator);
1617
1616
  }
1618
1617
 
1619
1618
  const rightComparison = getTypeofComparisonSide(node.right, node.left);
1620
- if (!rightComparison) return null;
1621
- return Object.assign({}, rightComparison, { operator: node.operator });
1619
+ if (rightComparison) {
1620
+ return normalizeTypeofComparison(rightComparison, node.operator, true);
1621
+ }
1622
+ return getBooleanTypeofComparison(node);
1623
+ }
1624
+
1625
+ function getBooleanTypeofComparison(node) {
1626
+ if (!isTypeofComparisonOperator(node.operator)) return null;
1627
+
1628
+ const isLeftBoolean = typeof node.left.value === "boolean";
1629
+ const valueNode = isLeftBoolean ? node.left : node.right;
1630
+ const expression = isLeftBoolean ? node.right : node.left;
1631
+ const isBoolean = valueNode.type === "Literal" && typeof valueNode.value === "boolean";
1632
+ if (!isBoolean) return null;
1633
+
1634
+ const comparison = getTypeofComparison(expression);
1635
+ if (!comparison) return null;
1636
+ const isEqual = node.operator === "===" || node.operator === "==";
1637
+ const preservesPolarity = isEqual === valueNode.value;
1638
+ const guardedBranch = getTypeofComparisonGuardBranch(comparison) === preservesPolarity;
1639
+ const operator = guardedBranch ? "!==" : "===";
1640
+ return { name: comparison.name, value: "undefined", operator };
1641
+ }
1642
+
1643
+ function normalizeTypeofComparison(comparison, operator, isReversed = false) {
1644
+ if (isTypeofComparisonOperator(operator)) {
1645
+ return Object.assign({}, comparison, { operator });
1646
+ }
1647
+
1648
+ const isRelational = operator === "<" || operator === ">";
1649
+ const isMinifiedGuard = isRelational && comparison.value === "u";
1650
+ if (!isMinifiedGuard) return null;
1651
+
1652
+ const isUndefined = (operator === ">") !== isReversed;
1653
+ const normalizedOperator = isUndefined ? "===" : "!==";
1654
+ return Object.assign({}, comparison, { value: "undefined", operator: normalizedOperator });
1622
1655
  }
1623
1656
 
1624
1657
  function isTypeofComparisonOperator(operator) {
@@ -1666,6 +1699,7 @@ function isFalseLiteral(node) {
1666
1699
  }
1667
1700
 
1668
1701
  function shouldSkipBranchGuard(node, branchValue) {
1702
+ if (node.type === "BinaryExpression") return true;
1669
1703
  const isLogical = node.type === "LogicalExpression";
1670
1704
  if (!isLogical) return false;
1671
1705
 
@@ -0,0 +1,89 @@
1
+ const { FEATURE_BROWSER_SUPPORT } = require("../constants/es-features/browserSupport");
2
+
3
+ function compareVersions(a, b) {
4
+ const aParts = String(a).split(".").map(Number);
5
+ const bParts = String(b).split(".").map(Number);
6
+ const length = Math.max(aParts.length, bParts.length);
7
+
8
+ let index = 0;
9
+ while (index < length) {
10
+ const diff = (aParts[index] || 0) - (bParts[index] || 0);
11
+ if (diff !== 0) return diff;
12
+ index += 1;
13
+ }
14
+
15
+ return 0;
16
+ }
17
+
18
+ function isVersionSupported(targetVersion, versionAdded) {
19
+ const isSupportedWithoutVersion = versionAdded === true;
20
+ if (isSupportedWithoutVersion) return true;
21
+
22
+ const isNeverSupported = versionAdded === false;
23
+ if (isNeverSupported) return false;
24
+
25
+ return compareVersions(targetVersion, versionAdded) >= 0;
26
+ }
27
+
28
+ function keepLowestVersionPerBrowser(browsers) {
29
+ const lowestByName = new Map();
30
+
31
+ browsers.forEach((browser) => {
32
+ const current = lowestByName.get(browser.name);
33
+ const isLower = !current || compareVersions(browser.version, current.version) < 0;
34
+ if (isLower) lowestByName.set(browser.name, browser);
35
+ });
36
+
37
+ return Array.from(lowestByName.values());
38
+ }
39
+
40
+ function getUnsupportedBrowsers(
41
+ featureName,
42
+ targetBrowsers,
43
+ browserSupport = FEATURE_BROWSER_SUPPORT,
44
+ ) {
45
+ const support = browserSupport[featureName];
46
+ const hasNoTargets = !targetBrowsers || targetBrowsers.length === 0;
47
+ const hasNoData = !support || hasNoTargets;
48
+ if (hasNoData) return [];
49
+
50
+ const unsupported = targetBrowsers.filter((browser) => {
51
+ const versionAdded = support[browser.name];
52
+ const hasBrowserData = versionAdded !== undefined;
53
+ if (!hasBrowserData) return false;
54
+ return !isVersionSupported(browser.version, versionAdded);
55
+ });
56
+
57
+ return keepLowestVersionPerBrowser(unsupported).map((browser) => ({
58
+ name: browser.name,
59
+ version: browser.version,
60
+ requiredVersion: support[browser.name],
61
+ }));
62
+ }
63
+
64
+ function formatBrowserRequirement({ name, version, requiredVersion }) {
65
+ const isNeverSupported = requiredVersion === false;
66
+ if (isNeverSupported) return `${name} ${version} has no support`;
67
+ return `${name} ${version} requires ${requiredVersion}`;
68
+ }
69
+
70
+ function formatFeatureWithBrowsers(featureName, unsupportedBrowsers) {
71
+ const hasNoBrowsers = !unsupportedBrowsers || unsupportedBrowsers.length === 0;
72
+ if (hasNoBrowsers) return featureName;
73
+
74
+ const requirements = unsupportedBrowsers.map(formatBrowserRequirement).join(", ");
75
+ return `${featureName} (${requirements})`;
76
+ }
77
+
78
+ function formatTargetBrowsers(targetBrowsers) {
79
+ return targetBrowsers.map((browser) => `${browser.name} ${browser.version}`).join(", ");
80
+ }
81
+
82
+ module.exports = {
83
+ compareVersions,
84
+ isVersionSupported,
85
+ getUnsupportedBrowsers,
86
+ formatBrowserRequirement,
87
+ formatFeatureWithBrowsers,
88
+ formatTargetBrowsers,
89
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-check",
3
- "version": "9.7.1",
3
+ "version": "9.8.0",
4
4
  "description": "Checks the ECMAScript version of .js glob against a specified version of ECMAScript with a shell command",
5
5
  "keywords": [
6
6
  "check for es6",
@@ -75,9 +75,10 @@
75
75
  "release:tag:dry": "node scripts/release-tag.mjs --dry-run",
76
76
  "release:dry": "release-it --dry-run --no-git.requireUpstream",
77
77
  "report:coverage": "mkdir -p coverage && node tests/scripts/test-runner.js unit --coverage --reporter=lcov --reporter-destination=coverage/lcov.info",
78
- "scripts:generate": "pnpm run scripts:generate:requirements && pnpm run scripts:generate:polyfills",
78
+ "scripts:generate": "pnpm run scripts:generate:requirements && pnpm run scripts:generate:polyfills && pnpm run scripts:generate:browser-support",
79
79
  "scripts:generate:requirements": "node scripts/generate/requirements/parser.mjs",
80
80
  "scripts:generate:polyfills": "node scripts/generate/polyfills/core.mjs",
81
+ "scripts:generate:browser-support": "node scripts/generate/browser-support/index.mjs",
81
82
  "test": "node tests/scripts/test-runner.js unit && npm run test:e2e",
82
83
  "test:unit": "node tests/scripts/test-runner.js unit",
83
84
  "test:unit:verbose": "node tests/scripts/test-runner.js unit --verbose",
@@ -100,7 +101,7 @@
100
101
  "fast-glob": "^3.3.3"
101
102
  },
102
103
  "devDependencies": {
103
- "@mdn/browser-compat-data": "^8.1.0",
104
+ "@mdn/browser-compat-data": "^8.1.2",
104
105
  "codependence": "1.0.15",
105
106
  "core-js-compat": "^3.50.0",
106
107
  "oxfmt": "^0.67.0",