es-check 9.3.1 → 9.4.0-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.
package/README.md CHANGED
@@ -31,7 +31,7 @@ Ensuring that JavaScript files can pass ES Check is important in a [modular and
31
31
  es-check es6 './dist/**/*.js' --checkFeatures
32
32
  ```
33
33
 
34
- ### `checkBrowser --browserslistQuery='<broswerslist query>'`
34
+ ### `checkBrowser --browserslistQuery='<browserslist query>'`
35
35
 
36
36
  ```sh
37
37
  es-check checkBrowser ./dist/**/*.js --browserslistQuery="last 2 versions"
@@ -134,6 +134,7 @@ Here's a comprehensive list of all available options:
134
134
  |--------|-------------|
135
135
  | `-V, --version` | Output the version number |
136
136
  | `--module` | Use ES modules (default: false) |
137
+ | `--light` | Lightweight mode: 2-3x faster checking using pattern matching only (default: false) |
137
138
  | `--allowHashBang` | If the code starts with #! treat it as a comment (default: false) |
138
139
  | `--files <files>` | A glob of files to test the ECMAScript version against (alias for [files...]) |
139
140
  | `--not <files>` | Folder or file names to skip |
@@ -201,6 +202,12 @@ Once enabled, you can use tab completion for:
201
202
  es-check es6 './dist/**/*.js' --module
202
203
  ```
203
204
 
205
+ **Fast checking with light mode (2-3x faster):**
206
+
207
+ ```sh
208
+ es-check es5 './dist/**/*.js' --light
209
+ ```
210
+
204
211
  **Checking files with hash bang:**
205
212
 
206
213
  ```sh
package/detectFeatures.js CHANGED
@@ -1,7 +1,5 @@
1
- const acorn = require('acorn');
2
- const walk = require('acorn-walk');
1
+ const fastBrake = require('fast-brake');
3
2
  const { ES_FEATURES, POLYFILL_PATTERNS, IMPORT_PATTERNS } = require('./constants');
4
- const { checkMap } = require('./utils');
5
3
 
6
4
  /**
7
5
  * Detects polyfills in the code and adds them to the polyfills Set
@@ -28,54 +26,105 @@ const detectPolyfills = (
28
26
  }
29
27
  }
30
28
 
29
+ const featureNameMap = {
30
+ 'let_const': ['let', 'const'],
31
+ 'arrow_functions': 'ArrowFunctions',
32
+ 'template_literals': 'TemplateLiterals',
33
+ 'destructuring': 'Destructuring',
34
+ 'classes': 'class',
35
+ 'extends': 'extends',
36
+ 'spread_rest': ['RestSpread', 'ArraySpread'],
37
+ 'default_parameters': 'DefaultParams',
38
+ 'default_params': 'DefaultParams',
39
+ 'for_of': 'ForOf',
40
+ 'import': 'import',
41
+ 'export': 'export',
42
+ 'import_export': ['import', 'export'],
43
+ 'async_await': 'AsyncAwait',
44
+ 'generators': 'Generators',
45
+ 'promise': 'Promise',
46
+ 'promise_resolve': 'PromiseResolve',
47
+ 'promise_reject': 'PromiseReject',
48
+ 'promise_any': 'PromiseAny',
49
+ 'promise_allSettled': 'PromiseAllSettled',
50
+ 'map': 'Map',
51
+ 'set': 'Set',
52
+ 'weakmap': 'WeakMap',
53
+ 'weakset': 'WeakSet',
54
+ 'symbol': 'Symbol',
55
+ 'proxy': 'Proxy',
56
+ 'reflect': 'Reflect',
57
+ 'weakref': 'WeakRef',
58
+ 'finalization_registry': 'FinalizationRegistry',
59
+ 'exponentiation': 'ExponentOperator',
60
+ 'object_spread': 'ObjectSpread',
61
+ 'rest_spread_properties': 'ObjectSpread',
62
+ 'optional_catch': 'OptionalCatchBinding',
63
+ 'bigint': 'BigInt',
64
+ 'nullish_coalescing': 'NullishCoalescing',
65
+ 'optional_chaining': 'OptionalChaining',
66
+ 'private_fields': 'PrivateClassFields',
67
+ 'logical_assignment': 'LogicalAssignment',
68
+ 'numeric_separators': 'NumericSeparators',
69
+ 'class_fields': 'ClassFields',
70
+ 'top_level_await': 'TopLevelAwait',
71
+ 'globalThis': 'globalThis',
72
+ 'array_at': 'ArrayPrototypeAt',
73
+ 'object_hasOwn': 'ObjectHasOwn',
74
+ 'string_replaceAll': 'StringReplaceAll'
75
+ };
76
+
31
77
  const detectFeatures = (code, ecmaVersion, sourceType, ignoreList = new Set(), options = {}) => {
32
- const { checkForPolyfills, ast: providedAst } = options;
78
+ const { checkForPolyfills, ast } = options;
33
79
 
34
80
  const polyfills = new Set();
35
81
  if (checkForPolyfills) detectPolyfills(code, polyfills);
36
82
 
37
- const ast = providedAst || acorn.parse(code, {
38
- ecmaVersion: 'latest',
39
- sourceType,
40
- });
41
-
42
- const allChecks = Object.entries(ES_FEATURES).map(([featureName, { astInfo }]) => ({
43
- featureName,
44
- nodeType: astInfo.nodeType,
45
- astInfo,
46
- }));
83
+ let detectedFeatures = [];
84
+
85
+ if (ast && ast.features) {
86
+ detectedFeatures = ast.features;
87
+ } else {
88
+ try {
89
+ const detectOptions = {
90
+ sourceType: sourceType || 'script'
91
+ };
92
+ detectedFeatures = fastBrake.detect(code, detectOptions);
93
+ } catch (err) {
94
+ const error = new Error(`Failed to parse code: ${err.message}`);
95
+ error.type = 'ES-Check';
96
+ throw error;
97
+ }
98
+ }
47
99
 
48
100
  const foundFeatures = Object.keys(ES_FEATURES).reduce((acc, f) => {
49
101
  acc[f] = false;
50
102
  return acc;
51
103
  }, {});
52
104
 
53
- const universalVisitor = (node) => {
54
- allChecks
55
- .filter(({ nodeType }) => nodeType === node.type)
56
- .forEach(({ featureName, astInfo }) => {
57
- const checker = checkMap[node.type] || checkMap.default;
58
- if (checker(node, astInfo)) {
59
- foundFeatures[featureName] = true;
60
- }
61
- });
62
- };
63
-
64
- const nodeTypes = [...new Set(allChecks.map((c) => c.nodeType))];
65
- const visitors = nodeTypes.reduce((acc, nt) => {
66
- acc[nt] = universalVisitor;
67
- return acc;
68
- }, {});
69
-
70
- walk.simple(ast, visitors);
105
+ detectedFeatures.forEach(feature => {
106
+ const mapped = featureNameMap[feature.name];
107
+
108
+ if (mapped) {
109
+ if (Array.isArray(mapped)) {
110
+ mapped.forEach(name => {
111
+ if (ES_FEATURES[name]) {
112
+ foundFeatures[name] = true;
113
+ }
114
+ });
115
+ } else if (ES_FEATURES[mapped]) {
116
+ foundFeatures[mapped] = true;
117
+ }
118
+ }
119
+ });
71
120
 
72
- const unsupportedFeatures = Object.entries(ES_FEATURES).reduce((acc = [], [featureName, { minVersion }]) => {
121
+ const unsupportedFeatures = [];
122
+ Object.entries(ES_FEATURES).forEach(([featureName, { minVersion }]) => {
73
123
  const isPolyfilled = checkForPolyfills && polyfills.has(featureName);
74
124
  if (foundFeatures[featureName] && minVersion > ecmaVersion && !ignoreList.has(featureName) && !isPolyfilled) {
75
- acc.push(featureName);
125
+ unsupportedFeatures.push(featureName);
76
126
  }
77
- return acc;
78
- }, []);
127
+ });
79
128
 
80
129
  if (unsupportedFeatures.length > 0) {
81
130
  const error = new Error(
package/index.js CHANGED
@@ -100,6 +100,7 @@ program
100
100
  .option('--config <path>', 'path to custom .escheckrc config file')
101
101
  .option('--batchSize <number>', 'number of files to process concurrently (0 for unlimited)', '0')
102
102
  .option('--noCache', 'disable file caching (caching is enabled by default)', false)
103
+ .option('--light', 'lightweight mode: faster checking with pattern matching only (skips full AST parsing)', false)
103
104
 
104
105
  async function loadConfig(customConfigPath) {
105
106
  const logger = createLogger();
@@ -182,6 +183,7 @@ program
182
183
  browserslistEnv: options.browserslistEnv !== undefined ? options.browserslistEnv : baseConfig.browserslistEnv,
183
184
  batchSize: options.batchSize !== undefined ? options.batchSize : baseConfig.batchSize,
184
185
  cache: options.noCache ? false : (baseConfig.cache !== undefined ? baseConfig.cache : true),
186
+ light: options.light !== undefined ? options.light : baseConfig.light,
185
187
  };
186
188
 
187
189
  if (ecmaVersionArg !== undefined) {
@@ -236,6 +238,7 @@ async function runChecks(configs, loggerOrOptions) {
236
238
  const checkFeatures = config.checkFeatures;
237
239
  const checkForPolyfills = config.checkForPolyfills;
238
240
  const checkBrowser = config.checkBrowser;
241
+ const lightMode = config.light;
239
242
  const ignoreFilePath = config.ignoreFile || config['ignore-file'];
240
243
 
241
244
  const ignoreFileExists = ignoreFilePath && fs.existsSync(ignoreFilePath);
@@ -475,10 +478,22 @@ async function runChecks(configs, loggerOrOptions) {
475
478
  logger.debug(`ES-Check: checking ${file}`)
476
479
  }
477
480
 
481
+ if (lightMode) {
482
+ const { parseLightMode } = require('./utils');
483
+ const { error: parseError } = await parseLightMode(code, ecmaVersion, esmodule, allowHashBang, file);
484
+ if (parseError) {
485
+ if (isDebug) {
486
+ logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${parseError.err}`)
487
+ }
488
+ return parseError;
489
+ }
490
+ return null;
491
+ }
492
+
478
493
  const needsFullAST = checkFeatures;
479
494
  const parserOptions = needsFullAST ? acornOpts : { ...acornOpts, locations: false, ranges: false, onComment: null };
480
495
 
481
- const { ast, error: parseError } = parseCode(code, parserOptions, acorn, file);
496
+ const { ast, error: parseError } = parseCode(code, parserOptions, acorn, file, checkFeatures);
482
497
  if (parseError) {
483
498
  if (isDebug) {
484
499
  logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${parseError.err}`)
@@ -571,6 +586,8 @@ async function runChecks(configs, loggerOrOptions) {
571
586
 
572
587
  if (isNodeAPI) {
573
588
  return { success: true, errors: [] };
589
+ } else {
590
+ process.exit(0);
574
591
  }
575
592
  }
576
593
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-check",
3
- "version": "9.3.1",
3
+ "version": "9.4.0-0",
4
4
  "description": "Checks the ECMAScript version of .js glob against a specified version of ECMAScript with a shell command",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -33,12 +33,14 @@
33
33
  "prepare": "husky",
34
34
  "prepublishOnly": "pnpm test",
35
35
  "release": "release-it --no-git.requireUpstream",
36
+ "changelog": "node scripts/update-changelog.mjs",
36
37
  "report:coverage": "nyc report --reporter=lcov > coverage.lcov && codecov",
37
38
  "setup": "pnpm install --reporter=silent",
38
39
  "test": "nyc mocha test.js utils.test.js browserslist.test.js polyfillDetector.test.js detectFeatures.test.js --timeout 10s && npm run test:e2e",
39
- "test:e2e": "npm run test:e2e:cli && npm run test:e2e:esm",
40
+ "test:e2e": "npm run test:e2e:cli && npm run test:e2e:esm && npm run test:e2e:package-files",
40
41
  "test:e2e:cli": "node e2e/test-cli.js",
41
42
  "test:e2e:esm": "node e2e/test-esm-import.mjs",
43
+ "test:e2e:package-files": "node e2e/test-package-files.mjs",
42
44
  "update": "codependence --update",
43
45
  "benchmark": "./benchmarks/run-benchmarks.sh",
44
46
  "site:dev": "pnpm --filter es-check-docs run dev",
@@ -68,10 +70,11 @@
68
70
  "codependence": "^0.3.1",
69
71
  "commitizen": "4.3.1",
70
72
  "conventional-changelog-cli": "^5.0.0",
71
- "eslint": "9.33.0",
73
+ "eslint": "9.34.0",
72
74
  "eslint-config-prettier": "10.1.8",
73
75
  "eslint-plugin-es5": "^1.5.0",
74
76
  "husky": "9.1.7",
77
+ "inquirer": "^12.9.1",
75
78
  "is-ci": "^3.0.1",
76
79
  "mocha": "11.7.1",
77
80
  "nyc": "^17.1.0",
@@ -84,6 +87,7 @@
84
87
  "acorn-walk": "^8.3.4",
85
88
  "browserslist": "^4.23.3",
86
89
  "commander": "14.0.0",
90
+ "fast-brake": "^0.1.1",
87
91
  "fast-glob": "^3.3.3",
88
92
  "lilconfig": "^3.1.3",
89
93
  "source-map": "^0.7.4",
@@ -144,7 +148,7 @@
144
148
  "pnpm run lint",
145
149
  "pnpm test"
146
150
  ],
147
- "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
151
+ "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}. && pnpm run changelog"
148
152
  }
149
153
  },
150
154
  "packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af"
package/utils.js CHANGED
@@ -420,51 +420,117 @@ function getFileCacheStats() {
420
420
  return fileCache.getStats();
421
421
  }
422
422
 
423
+ const ECMA_VERSION_MAP = {
424
+ 5: 'es5',
425
+ 6: 'es2015',
426
+ 7: 'es2016',
427
+ 8: 'es2017',
428
+ 9: 'es2018',
429
+ 10: 'es2019',
430
+ 11: 'es2020',
431
+ 12: 'es2021',
432
+ 13: 'es2022',
433
+ 14: 'es2023',
434
+ 15: 'es2024',
435
+ 16: 'es2025',
436
+ 2015: 'es2015',
437
+ 2016: 'es2016',
438
+ 2017: 'es2017',
439
+ 2018: 'es2018',
440
+ 2019: 'es2019',
441
+ 2020: 'es2020',
442
+ 2021: 'es2021',
443
+ 2022: 'es2022',
444
+ 2023: 'es2023',
445
+ 2024: 'es2024',
446
+ 2025: 'es2025'
447
+ };
448
+
423
449
  /**
424
- * Parse code with acorn and handle errors
450
+ * Convert ecmaVersion to fast-brake target format
451
+ * @param {number|string} ecmaVersion - Version from acorn options
452
+ * @returns {string} Target version for fast-brake
453
+ */
454
+ function getTargetVersion(ecmaVersion) {
455
+ return ECMA_VERSION_MAP[ecmaVersion] || 'es5';
456
+ }
457
+
458
+ /**
459
+ * Parse code with fast-brake and handle errors
425
460
  * @param {string} code - Code to parse
426
- * @param {Object} acornOpts - Acorn parsing options
427
- * @param {Object} acorn - Acorn module
461
+ * @param {Object} acornOpts - Parsing options (for compatibility)
462
+ * @param {Object} acorn - Module (for compatibility, not used)
428
463
  * @param {string} file - File path for error reporting
429
464
  * @returns {{ast: Object, error: null} | {ast: null, error: Object}}
430
465
  */
431
- function parseCode(code, acornOpts, acorn, file) {
466
+ const fastBrake = require('fast-brake');
467
+
468
+ const parseCache = new Map();
469
+
470
+ function parseCode(code, acornOpts, acorn, file, needsFeatures = false) {
471
+ const cacheKey = `${file}:${acornOpts.ecmaVersion}:${acornOpts.sourceType}:${needsFeatures}:${code.length}`;
472
+
473
+ if (parseCache.has(cacheKey)) {
474
+ return parseCache.get(cacheKey);
475
+ }
476
+
432
477
  try {
433
- const ast = acorn.parse(code, acornOpts);
434
- return { ast, error: null };
478
+ const ecmaVersion = acornOpts.ecmaVersion || 5;
479
+ const targetVersion = getTargetVersion(ecmaVersion);
480
+ const sourceType = acornOpts.sourceType || 'script';
481
+
482
+ const codeToCheck = acornOpts.allowHashBang && code.startsWith('#!')
483
+ ? code.slice(code.indexOf('\n') + 1)
484
+ : code;
485
+
486
+ const options = { target: targetVersion, sourceType };
487
+
488
+ if (sourceType !== 'module') {
489
+ const quickCheck = fastBrake.detect(codeToCheck, { sourceType: 'script' });
490
+ const moduleFeature = quickCheck.find(f => f.name === 'import' || f.name === 'export');
491
+
492
+ if (moduleFeature) {
493
+ throw new Error(
494
+ `'${moduleFeature.name}' can only be used in ES modules. Use --module flag to enable module support` +
495
+ (moduleFeature.line ? ` at line ${moduleFeature.line}` : '')
496
+ );
497
+ }
498
+ }
499
+
500
+ fastBrake.fastBrake(codeToCheck, options);
501
+
502
+ const detectedFeatures = needsFeatures
503
+ ? fastBrake.detect(codeToCheck, { sourceType })
504
+ : [];
505
+
506
+ const result = {
507
+ ast: { type: 'Program', features: detectedFeatures },
508
+ error: null
509
+ };
510
+ parseCache.set(cacheKey, result);
511
+ return result;
435
512
  } catch (err) {
436
- return {
513
+ const result = {
437
514
  ast: null,
438
- error: {
439
- err,
440
- stack: err.stack,
441
- file
442
- }
515
+ error: { err, stack: err.stack, file }
443
516
  };
517
+ parseCache.set(cacheKey, result);
518
+ return result;
444
519
  }
445
520
  }
446
521
 
447
- /**
448
- * Determine how runChecks is being invoked (CLI vs Node API)
449
- * @param {Object|null} loggerOrOptions - Logger or options object passed to runChecks
450
- * @returns {{isNodeAPI: boolean, logger: Object|null}}
451
- */
452
522
  function determineInvocationType(loggerOrOptions) {
453
- let isNodeAPI = false;
454
- let logger = null;
455
-
456
523
  if (!loggerOrOptions) {
457
- isNodeAPI = true;
458
- logger = null;
459
- } else if (typeof loggerOrOptions === 'object' && !loggerOrOptions.info && !loggerOrOptions.error) {
460
- isNodeAPI = true;
461
- logger = loggerOrOptions.logger || null;
462
- } else {
463
- isNodeAPI = false;
464
- logger = loggerOrOptions;
524
+ return { isNodeAPI: true, logger: null };
465
525
  }
466
526
 
467
- return { isNodeAPI, logger };
527
+ const hasLoggerMethods = loggerOrOptions.info || loggerOrOptions.error;
528
+
529
+ if (typeof loggerOrOptions === 'object' && !hasLoggerMethods) {
530
+ return { isNodeAPI: true, logger: loggerOrOptions.logger || null };
531
+ }
532
+
533
+ return { isNodeAPI: false, logger: loggerOrOptions };
468
534
  }
469
535
 
470
536
  /**
@@ -515,6 +581,37 @@ function handleESVersionError(options) {
515
581
  }
516
582
  }
517
583
 
584
+ (async () => {
585
+ try {
586
+ await fastBrake.check('', { target: 'es5' });
587
+ } catch (e) {}
588
+ })();
589
+
590
+ async function parseLightMode(code, ecmaVersion, isModule, allowHashBang, file) {
591
+ const targetVersion = getTargetVersion(ecmaVersion);
592
+
593
+ const codeToCheck = allowHashBang && code.startsWith('#!')
594
+ ? code.slice(code.indexOf('\n') + 1)
595
+ : code;
596
+
597
+ const isCompatible = await fastBrake.check(codeToCheck, {
598
+ target: targetVersion,
599
+ sourceType: isModule ? 'module' : 'script'
600
+ });
601
+
602
+ if (!isCompatible) {
603
+ return {
604
+ error: {
605
+ err: new Error(`Code contains features incompatible with ${targetVersion}`),
606
+ stack: '',
607
+ file
608
+ }
609
+ };
610
+ }
611
+
612
+ return { error: null };
613
+ }
614
+
518
615
  module.exports = {
519
616
  parseIgnoreList,
520
617
  checkVarKindMatch,
@@ -530,6 +627,7 @@ module.exports = {
530
627
  clearFileCache,
531
628
  getFileCacheStats,
532
629
  parseCode,
630
+ parseLightMode,
533
631
  determineInvocationType,
534
632
  determineLogLevel,
535
633
  handleESVersionError