es-check 9.1.4 → 9.2.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
@@ -24,10 +24,19 @@ Ensuring that JavaScript files can pass ES Check is important in a [modular and
24
24
 
25
25
  **ES Check** version 9 is a major release update that can enforce more ES version specific features checks, implements initial browserslist integration, basic (naive) polyfill detection, and supports an allowlist. To enable ecmaVersion specific checks, pass the `--checkFeatures` flag. To enable browserslist integration, pass the `--checkBrowser` flag. To enable polyfill detection, pass the `--checkForPolyfills` flag. There is also more config file support. Besides this, there are other feature updates based on user feedback. This version should not break any existing scripts but, as significant changes/features have been added and it's know that es-check supports protecting against breaking errors going to production, a major version bump feels appropriate. Please report any issues!
26
26
 
27
+
28
+ ### `--checkFeatures`
29
+
27
30
  ```sh
28
31
  es-check es6 './dist/**/*.js' --checkFeatures
29
32
  ```
30
33
 
34
+ ### `checkBrowser --browserslistQuery='<broswerslist query>'`
35
+
36
+ ```sh
37
+ es-check checkBrowser ./dist/**/*.js --browserslistQuery="last 2 versions"
38
+ ```
39
+
31
40
  ---
32
41
 
33
42
  <p align="center">
@@ -82,22 +91,20 @@ ES Check checks syntax out of the box—to protect against breaking errors going
82
91
 
83
92
  The images below demonstrate command line scripts and their corresponding logged results.
84
93
 
85
- Pass
94
+ ### Pass
95
+
86
96
  ![pass](https://user-images.githubusercontent.com/1074042/31471487-d7be22ee-ae9d-11e7-86e2-2c0f71cfffe6.jpg)
87
97
 
88
- Fail
98
+ ### Fail
99
+
89
100
  ![fail](https://user-images.githubusercontent.com/1074042/31471486-d65c3a80-ae9d-11e7-94fd-68b7acdb2d89.jpg)
90
101
 
91
- **ES Check** is run above with node commands. It can also be run within npm scripts, ci tools, or testing suites.
102
+ **ES Check** is run above with node commands. It can also be run within npm scripts, ci tools, or testing suites. It also provide minimal support for use in node apps.
92
103
 
93
104
  ---
94
105
 
95
106
  ## API
96
107
 
97
- **ES Check** provides the necessities. It accepts its place as a JavaScript matcher/tester.
98
-
99
- ### General Information
100
-
101
108
  ```sh
102
109
 
103
110
  # USAGE
@@ -145,6 +152,7 @@ Here's a comprehensive list of all available options:
145
152
  | `--browserslistPath <path>` | Path to custom browserslist configuration (default: uses standard browserslist config resolution) |
146
153
  | `--browserslistEnv <env>` | Browserslist environment to use (default: production) |
147
154
  | `--config <path>` | Path to custom .escheckrc config file |
155
+ | `--batchSize <number>` | Number of files to process concurrently (0 for unlimited, default: 0) |
148
156
  | `-h, --help` | Display help for command |
149
157
 
150
158
  ### Shell Completion
@@ -264,7 +272,7 @@ es-check checkBrowser ./dist/**/*.js
264
272
 
265
273
  ## Usage
266
274
 
267
- ES Check is a shell command CLI. It is run in [shell tool](http://linuxcommand.org/lc3_learning_the_shell.php) like Terminal, ITerm, or Hyper. It takes in two arguments: an [ECMAScript version](https://www.w3schools.com/js/js_versions.asp) (`<ECMAScript version>`) and files (`[files]`) in [globs](http://searchsecurity.techtarget.com/definition/globbing).
275
+ ES Check is mainly a shell command CLI. It is run in [shell tool](http://linuxcommand.org/lc3_learning_the_shell.php) like Terminal, ITerm, or Hyper. It takes in two arguments: an [ECMAScript version](https://www.w3schools.com/js/js_versions.asp) (`<ECMAScript version>`) and files (`[files]`) in [globs](http://searchsecurity.techtarget.com/definition/globbing).
268
276
 
269
277
  Here are some example of **es check** scripts that could be run:
270
278
 
@@ -276,6 +284,36 @@ es-check es6 ./js/*.js
276
284
  es-check es6 ./js/*.js ./dist/*.js
277
285
  ```
278
286
 
287
+ ### Using ES Check in Node
288
+
289
+ In addition to its CLI utility, ES Check can be used programmatically in Node.js applications:
290
+
291
+ ```javascript
292
+ const { runChecks, loadConfig } = require('es-check');
293
+
294
+ async function checkMyFiles() {
295
+ const config = {
296
+ ecmaVersion: 'es5',
297
+ files: ['dist/**/*.js'],
298
+ module: false,
299
+ checkFeatures: true
300
+ };
301
+
302
+ try {
303
+ await runChecks([config], logger);
304
+ console.log('All files passed ES5 check!');
305
+ } catch (error) {
306
+ console.error('Some files failed the ES check');
307
+ process.exit(1);
308
+ }
309
+ }
310
+
311
+ async function checkWithConfig() {
312
+ const configs = await loadConfig('./.escheckrc');
313
+ await runChecks(configs, logger);
314
+ }
315
+ ```
316
+
279
317
  ---
280
318
 
281
319
  ## Configuration
@@ -485,6 +523,40 @@ es-check --checkBrowser --checkFeatures ./dist/**/*.js
485
523
 
486
524
  ---
487
525
 
526
+ ## Performance Optimization
527
+
528
+ ES Check provides the `--batchSize` option to optimize performance for different scenarios:
529
+
530
+ ```sh
531
+ # Process all files in parallel (default)
532
+ es-check es5 './dist/**/*.js' --batchSize 0
533
+
534
+ # Process 10 files at a time (memory-constrained environments)
535
+ es-check es5 './dist/**/*.js' --batchSize 10
536
+
537
+ # Process 50 files at a time (balanced approach)
538
+ es-check es5 './dist/**/*.js' --batchSize 50
539
+ ```
540
+
541
+ ### Performance Guidelines
542
+
543
+ | Scenario | Recommended `--batchSize` | Reason |
544
+ |----------|---------------------------|---------|
545
+ | Small codebases (< 100 files) | `0` (unlimited) | Maximum parallelism for fastest results |
546
+ | Medium codebases (100-500 files) | `0` or `50` | Balance between speed and memory |
547
+ | Large codebases (> 500 files) | `50-100` | Prevent memory spikes |
548
+ | CI/CD with limited memory | `10-20` | Conservative memory usage |
549
+ | Local development | `0` (default) | Utilize available hardware |
550
+
551
+ ### Recent Performance Improvements
552
+
553
+ As of August 2025, ES Check has been optimized with:
554
+ - **Single-parse optimization**: Files are parsed once and the AST is reused
555
+ - **Async file processing**: Non-blocking I/O for better performance
556
+ - **Configurable batch processing**: Fine-tune based on your needs
557
+
558
+ ---
559
+
488
560
  ## Checking node_modules Dependencies
489
561
 
490
562
  To check node_modules dependencies for ES compatibility:
@@ -503,13 +575,13 @@ A simple example script is available in `examples/check-node-modules.js`.
503
575
 
504
576
  ## Acknowledgements
505
577
 
506
- ES Check is a small utility using powerful tools that [Isaac Z. Schlueter](https://github.com/isaacs), [Marijn Haverbeke](https://github.com/marijnh), and [Matthias Etienne](https://github.com/mattallty) built. [ES Checker](https://github.com/ruanyf/es-checker) by [Ruan YiFeng](https://github.com/ruanyf) checks the JavaScript version supported within a [browser](http://ruanyf.github.io/es-checker/) at run time. ES Check offers similar feedback to ES Checker but at build time and is specific to the product that is using it. ES Check was started after reading this [post](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) about [deploying es2015 code to production today] by [Philip Walton](https://github.com/philipwalton).
578
+ ES Check is a small utility using powerful tools that [Isaac Z. Schlueter](https://github.com/isaacs), [Marijn Haverbeke](https://github.com/marijnh), and [Matthias Etienne](https://github.com/mattallty) built. [ES Checker](https://github.com/ruanyf/es-checker) by [Ruan YiFeng](https://github.com/ruanyf) checks the JavaScript version supported within a [browser](http://ruanyf.github.io/es-checker/) at run time. ES Check offers similar feedback to ES Checker but at build time and is specific to the product that is using it. ES Check was started after reading [Philip Walton](https://github.com/philipwalton)'s post about [deploying es2015 code to production today](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/).
507
579
 
508
580
  ---
509
581
 
510
582
  ## Contributing
511
583
 
512
- ES Check has 7 dependencies: [acorn and acorn-walk](https://github.com/ternjs/acorn/), [fast-glob](https://github.com/mrmlnc/fast-glob), [supports-color](github.com/chalk/supports-color), [winston](https://github.com/winstonjs/winston), [browserslist](https://github.com/browserslist/browserslist), and [commander](https://github.com/tj/commander). To contribute, file an [issue](https://github.com/yowainwright/es-check/issues) or submit a pull request.
584
+ ES Check has 7 dependencies: [acorn and acorn-walk](https://github.com/ternjs/acorn/), [fast-glob](https://github.com/mrmlnc/fast-glob), [supports-color](https://github.com/chalk/supports-color), [winston](https://github.com/winstonjs/winston), [browserslist](https://github.com/browserslist/browserslist), and [commander](https://github.com/tj/commander). To contribute, file an [issue](https://github.com/yowainwright/es-check/issues) or submit a pull request.
513
585
 
514
586
  To update es versions, check out these lines of code [here](https://github.com/yowainwright/es-check/blob/main/index.js#L92-L153) and [here (in acorn.js)](https://github.com/acornjs/acorn/blob/3221fa54f9dea30338228b97210c4f1fd332652d/acorn/src/acorn.d.ts#L586).
515
587
 
package/constants.js CHANGED
@@ -502,6 +502,100 @@ const ES_FEATURES = {
502
502
  property: 'with',
503
503
  },
504
504
  },
505
+
506
+ // ----------------------------------------------------------
507
+ // ES14 / ES2023 (continued)
508
+ // ----------------------------------------------------------
509
+ ArrayFindLast: {
510
+ minVersion: 14,
511
+ example: 'arr.findLast(x => x > 5)',
512
+ astInfo: {
513
+ nodeType: 'CallExpression',
514
+ property: 'findLast',
515
+ },
516
+ },
517
+ ArrayFindLastIndex: {
518
+ minVersion: 14,
519
+ example: 'arr.findLastIndex(x => x > 5)',
520
+ astInfo: {
521
+ nodeType: 'CallExpression',
522
+ property: 'findLastIndex',
523
+ },
524
+ },
525
+
526
+ // ----------------------------------------------------------
527
+ // ES15 / ES2024
528
+ // ----------------------------------------------------------
529
+ ObjectHasOwn: {
530
+ minVersion: 13,
531
+ example: 'Object.hasOwn(obj, "prop")',
532
+ astInfo: {
533
+ nodeType: 'CallExpression',
534
+ object: 'Object',
535
+ property: 'hasOwn',
536
+ },
537
+ },
538
+ StringIsWellFormed: {
539
+ minVersion: 15,
540
+ example: 'str.isWellFormed()',
541
+ astInfo: {
542
+ nodeType: 'CallExpression',
543
+ property: 'isWellFormed',
544
+ },
545
+ },
546
+ StringToWellFormed: {
547
+ minVersion: 15,
548
+ example: 'str.toWellFormed()',
549
+ astInfo: {
550
+ nodeType: 'CallExpression',
551
+ property: 'toWellFormed',
552
+ },
553
+ },
554
+ RegExpUnicodeSetFlag: {
555
+ minVersion: 15,
556
+ example: '/pattern/v',
557
+ astInfo: {
558
+ nodeType: 'RegExpLiteral',
559
+ flags: 'v',
560
+ },
561
+ },
562
+
563
+ // ----------------------------------------------------------
564
+ // ES16 / ES2025
565
+ // ----------------------------------------------------------
566
+ ArrayGroup: {
567
+ minVersion: 16,
568
+ example: 'arr.group(x => x.category)',
569
+ astInfo: {
570
+ nodeType: 'CallExpression',
571
+ property: 'group',
572
+ },
573
+ },
574
+ ArrayGroupToMap: {
575
+ minVersion: 16,
576
+ example: 'arr.groupToMap(x => x.category)',
577
+ astInfo: {
578
+ nodeType: 'CallExpression',
579
+ property: 'groupToMap',
580
+ },
581
+ },
582
+ PromiseTry: {
583
+ minVersion: 16,
584
+ example: 'Promise.try(() => syncOrAsyncFunction())',
585
+ astInfo: {
586
+ nodeType: 'CallExpression',
587
+ object: 'Promise',
588
+ property: 'try',
589
+ },
590
+ },
591
+ DuplicateNamedCaptureGroups: {
592
+ minVersion: 16,
593
+ example: '/(?<name>a)|(?<name>b)/',
594
+ astInfo: {
595
+ nodeType: 'RegExpLiteral',
596
+ duplicateNamedGroups: true,
597
+ },
598
+ },
505
599
  };
506
600
 
507
601
  const NODE_TYPES = {
@@ -512,6 +606,45 @@ const NODE_TYPES = {
512
606
  NEW_EXPRESSION: 'NewExpression',
513
607
  };
514
608
 
609
+ const POLYFILL_PATTERNS = [
610
+ // Array methods
611
+ { pattern: /Array\.prototype\.toSorted/, feature: 'ArrayToSorted' },
612
+ { pattern: /Array\.prototype\.findLast/, feature: 'ArrayFindLast' },
613
+ { pattern: /Array\.prototype\.findLastIndex/, feature: 'ArrayFindLastIndex' },
614
+ { pattern: /Array\.prototype\.at/, feature: 'ArrayAt' },
615
+
616
+ // String methods
617
+ { pattern: /String\.prototype\.replaceAll/, feature: 'StringReplaceAll' },
618
+ { pattern: /String\.prototype\.matchAll/, feature: 'StringMatchAll' },
619
+ { pattern: /String\.prototype\.at/, feature: 'StringAt' },
620
+
621
+ // Object methods
622
+ { pattern: /Object\.hasOwn/, feature: 'ObjectHasOwn' },
623
+
624
+ // Promise methods
625
+ { pattern: /Promise\.any/, feature: 'PromiseAny' },
626
+
627
+ // RegExp methods
628
+ { pattern: /RegExp\.prototype\.exec/, feature: 'RegExpExec' },
629
+
630
+ // Global methods
631
+ { pattern: /globalThis/, feature: 'GlobalThis' },
632
+ ];
633
+
634
+ const IMPORT_PATTERNS = [
635
+ { pattern: /from\s+['"]core-js\/modules\/es\.array\.to-sorted['"]/, feature: 'ArrayToSorted' },
636
+ { pattern: /from\s+['"]core-js\/modules\/es\.array\.find-last['"]/, feature: 'ArrayFindLast' },
637
+ { pattern: /from\s+['"]core-js\/modules\/es\.array\.find-last-index['"]/, feature: 'ArrayFindLastIndex' },
638
+ { pattern: /from\s+['"]core-js\/modules\/es\.array\.at['"]/, feature: 'ArrayAt' },
639
+ { pattern: /from\s+['"]core-js\/modules\/es\.string\.replace-all['"]/, feature: 'StringReplaceAll' },
640
+ { pattern: /from\s+['"]core-js\/modules\/es\.string\.match-all['"]/, feature: 'StringMatchAll' },
641
+ { pattern: /from\s+['"]core-js\/modules\/es\.string\.at['"]/, feature: 'StringAt' },
642
+ { pattern: /from\s+['"]core-js\/modules\/es\.object\.has-own['"]/, feature: 'ObjectHasOwn' },
643
+ { pattern: /from\s+['"]core-js\/modules\/es\.promise\.any['"]/, feature: 'PromiseAny' },
644
+ { pattern: /from\s+['"]core-js\/modules\/es\.regexp\.exec['"]/, feature: 'RegExpExec' },
645
+ { pattern: /from\s+['"]core-js\/modules\/es\.global-this['"]/, feature: 'GlobalThis' },
646
+ ];
647
+
515
648
  const BROWSER_TO_ES_VERSION = {
516
649
  ie: {
517
650
  '11': 5
@@ -578,74 +711,88 @@ const JS_VERSIONS = [
578
711
  'es3', 'es4', 'es5', 'es6', 'es2015',
579
712
  'es7', 'es2016', 'es8', 'es2017', 'es9', 'es2018',
580
713
  'es10', 'es2019', 'es11', 'es2020', 'es12', 'es2021',
581
- 'es13', 'es2022', 'es14', 'es2023', 'checkBrowser'
714
+ 'es13', 'es2022', 'es14', 'es2023', 'es15', 'es2024', 'es16', 'es2025', 'checkBrowser'
582
715
  ];
583
716
 
584
717
  /**
585
- * Maps feature names from ES_FEATURES to their polyfill patterns
586
- * This helps us identify which features are being polyfilled
718
+ * Maps feature names from ES_FEATURES to their polyfill patterns.
719
+ * This version uses standardized keys and robust regex for both manual and module polyfills.
587
720
  */
588
721
  const FEATURE_TO_POLYFILL_MAP = {
589
- // Array methods
590
- 'ArrayToSorted': [
591
- /Array\.prototype\.toSorted/,
592
- /from\s+['"]core-js\/modules\/es\.array\.to-sorted['"]/
593
- ],
594
- 'ArrayFindLast': [
595
- /Array\.prototype\.findLast/,
596
- /from\s+['"]core-js\/modules\/es\.array\.find-last['"]/
597
- ],
598
- 'ArrayFindLastIndex': [
599
- /Array\.prototype\.findLastIndex/,
600
- /from\s+['"]core-js\/modules\/es\.array\.find-last-index['"]/
601
- ],
602
- 'ArrayAt': [
603
- /Array\.prototype\.at/,
604
- /from\s+['"]core-js\/modules\/es\.array\.at['"]/
605
- ],
722
+ // ES2015 (ES6)
723
+ 'Array.from': [/\bArray\.from\s*=/, /['"]core-js\/modules\/es\.array\.from['"]/],
724
+ 'Array.of': [/\bArray\.of\s*=/, /['"]core-js\/modules\/es\.array\.of['"]/],
725
+ 'Array.prototype.find': [/\bArray\.prototype\.find\s*=/, /['"]core-js\/modules\/es\.array\.find['"]/],
726
+ 'Array.prototype.findIndex': [/\bArray\.prototype\.findIndex\s*=/, /['"]core-js\/modules\/es\.array\.find-index['"]/],
727
+ 'Object.assign': [/\bObject\.assign\s*=/, /['"]core-js\/modules\/es\.object\.assign['"]/, /object-assign/],
728
+ 'Promise': [/\bPromise\s*=/, /['"]core-js\/modules\/es\.promise['"]/, /es6-promise/],
729
+ 'String.prototype.startsWith': [/\bString\.prototype\.startsWith\s*=/, /['"]core-js\/modules\/es\.string\.starts-with['"]/],
730
+ 'String.prototype.endsWith': [/\bString\.prototype\.endsWith\s*=/, /['"]core-js\/modules\/es\.string\.ends-with['"]/],
731
+ 'String.prototype.includes': [/\bString\.prototype\.includes\s*=/, /['"]core-js\/modules\/es\.string\.includes['"]/],
732
+ 'Symbol': [/\bSymbol\s*=/, /['"]core-js\/modules\/es\.symbol['"]/],
733
+ 'Map': [/\bMap\s*=/, /['"]core-js\/modules\/es\.map['"]/],
734
+ 'Set': [/\bSet\s*=/, /['"]core-js\/modules\/es\.set['"]/],
735
+ 'WeakMap': [/\bWeakMap\s*=/, /['"]core-js\/modules\/es\.weak-map['"]/],
736
+ 'WeakSet': [/\bWeakSet\s*=/, /['"]core-js\/modules\/es\.weak-set['"]/],
606
737
 
607
- // String methods
608
- 'StringReplaceAll': [
609
- /String\.prototype\.replaceAll/,
610
- /from\s+['"]core-js\/modules\/es\.string\.replace-all['"]/
611
- ],
612
- 'StringMatchAll': [
613
- /String\.prototype\.matchAll/,
614
- /from\s+['"]core-js\/modules\/es\.string\.match-all['"]/
615
- ],
616
- 'StringAt': [
617
- /String\.prototype\.at/,
618
- /from\s+['"]core-js\/modules\/es\.string\.at['"]/
619
- ],
738
+ // ES2016
739
+ 'Array.prototype.includes': [/\bArray\.prototype\.includes\s*=/, /['"]core-js\/modules\/es\.array\.includes['"]/],
620
740
 
621
- // Object methods
622
- 'ObjectHasOwn': [
623
- /Object\.hasOwn/,
624
- /from\s+['"]core-js\/modules\/es\.object\.has-own['"]/
625
- ],
741
+ // ES2017
742
+ 'Object.values': [/\bObject\.values\s*=/, /['"]core-js\/modules\/es\.object\.values['"]/],
743
+ 'Object.entries': [/\bObject\.entries\s*=/, /['"]core-js\/modules\/es\.object\.entries['"]/],
744
+ 'Object.getOwnPropertyDescriptors': [/\bObject\.getOwnPropertyDescriptors\s*=/, /['"]core-js\/modules\/es\.object\.get-own-property-descriptors['"]/],
745
+ 'String.prototype.padStart': [/\bString\.prototype\.padStart\s*=/, /['"]core-js\/modules\/es\.string\.pad-start['"]/],
746
+ 'String.prototype.padEnd': [/\bString\.prototype\.padEnd\s*=/, /['"]core-js\/modules\/es\.string\.pad-end['"]/],
626
747
 
627
- // Promise methods
628
- 'PromiseAny': [
629
- /Promise\.any/,
630
- /from\s+['"]core-js\/modules\/es\.promise\.any['"]/
631
- ],
748
+ // ES2018
749
+ 'Promise.prototype.finally': [/\bPromise\.prototype\.finally\s*=/, /['"]core-js\/modules\/es\.promise\.finally['"]/],
632
750
 
633
- // RegExp methods
634
- 'RegExpExec': [
635
- /RegExp\.prototype\.exec/,
636
- /from\s+['"]core-js\/modules\/es\.regexp\.exec['"]/
637
- ],
751
+ // ES2019
752
+ 'Array.prototype.flat': [/\bArray\.prototype\.flat\s*=/, /['"]core-js\/modules\/es\.array\.flat['"]/],
753
+ 'Array.prototype.flatMap': [/\bArray\.prototype\.flatMap\s*=/, /['"]core-js\/modules\/es\.array\.flat-map['"]/],
754
+ 'Object.fromEntries': [/\bObject\.fromEntries\s*=/, /['"]core-js\/modules\/es\.object\.from-entries['"]/],
755
+ 'String.prototype.trimStart': [/\bString\.prototype\.trimStart\s*=/, /['"]core-js\/modules\/es\.string\.trim-start['"]/],
756
+ 'String.prototype.trimEnd': [/\bString\.prototype\.trimEnd\s*=/, /['"]core-js\/modules\/es\.string\.trim-end['"]/],
638
757
 
639
- // Global methods
640
- 'GlobalThis': [
641
- /globalThis/,
642
- /from\s+['"]core-js\/modules\/es\.global-this['"]/
643
- ],
758
+ // ES2020
759
+ 'Promise.allSettled': [/\bPromise\.allSettled\s*=/, /['"]core-js\/modules\/es\.promise\.all-settled['"]/],
760
+ 'String.prototype.matchAll': [/\bString\.prototype\.matchAll\s*=/, /['"]core-js\/modules\/es\.string\.match-all['"]/],
761
+ 'globalThis': [/globalThis\s*=/, /['"]core-js\/modules\/es\.global-this['"]/],
762
+ 'BigInt': [/\bBigInt\s*=/, /['"]core-js\/modules\/es\.bigint['"]/],
763
+
764
+ // ES2021
765
+ 'Promise.any': [/\bPromise\.any\s*=/, /['"]core-js\/modules\/es\.promise\.any['"]/],
766
+ 'String.prototype.replaceAll': [/\bString\.prototype\.replaceAll\s*=/, /['"]core-js\/modules\/es\.string\.replace-all['"]/],
767
+
768
+ // ES2022
769
+ 'Array.prototype.at': [/\bArray\.prototype\.at\s*=/, /['"]core-js\/modules\/es\.array\.at['"]/],
770
+ 'String.prototype.at': [/\bString\.prototype\.at\s*=/, /['"]core-js\/modules\/es\.string\.at['"]/],
771
+
772
+ // ES2023
773
+ 'Array.prototype.findLast': [/\bArray\.prototype\.findLast\s*=/, /['"]core-js\/modules\/es\.array\.find-last['"]/],
774
+ 'Array.prototype.findLastIndex': [/\bArray\.prototype\.findLastIndex\s*=/, /['"]core-js\/modules\/es\.array\.find-last-index['"]/],
775
+ 'Array.prototype.toReversed': [/\bArray\.prototype\.toReversed\s*=/, /['"]core-js\/modules\/es\.array\.to-reversed['"]/],
776
+ 'Array.prototype.toSorted': [/\bArray\.prototype\.toSorted\s*=/, /['"]core-js\/modules\/es\.array\.to-sorted['"]/],
777
+ 'Array.prototype.toSpliced': [/\bArray\.prototype\.toSpliced\s*=/, /['"]core-js\/modules\/es\.array\.to-spliced['"]/],
778
+ 'Array.prototype.with': [/\bArray\.prototype\.with\s*=/, /['"]core-js\/modules\/es\.array\.with['"]/],
779
+
780
+ // ES2024
781
+ 'Object.hasOwn': [/\bObject\.hasOwn\s*=/, /['"]core-js\/modules\/es\.object\.has-own['"]/],
782
+ 'String.prototype.isWellFormed': [/\bString\.prototype\.isWellFormed\s*=/, /['"]core-js\/modules\/es\.string\.is-well-formed['"]/],
783
+ 'String.prototype.toWellFormed': [/\bString\.prototype\.toWellFormed\s*=/, /['"]core-js\/modules\/es\.string\.to-well-formed['"]/],
784
+
785
+ // ES2025
786
+ 'Array.prototype.group': [/\bArray\.prototype\.group\s*=/, /['"]core-js\/modules\/es\.array\.group['"]/],
787
+ 'Array.prototype.groupToMap': [/\bArray\.prototype\.groupToMap\s*=/, /['"]core-js\/modules\/es\.array\.group-to-map['"]/],
788
+ 'Promise.try': [/\bPromise\.try\s*=/, /['"]core-js\/modules\/es\.promise\.try['"]/],
644
789
  };
645
790
 
646
791
  module.exports = {
647
792
  ES_FEATURES,
648
793
  NODE_TYPES,
794
+ POLYFILL_PATTERNS,
795
+ IMPORT_PATTERNS,
649
796
  BROWSER_TO_ES_VERSION,
650
797
  FEATURE_TO_POLYFILL_MAP,
651
798
  JS_VERSIONS,
package/detectFeatures.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const acorn = require('acorn');
2
2
  const walk = require('acorn-walk');
3
- const { ES_FEATURES } = require('./constants');
3
+ const { ES_FEATURES, POLYFILL_PATTERNS, IMPORT_PATTERNS } = require('./constants');
4
4
  const { checkMap } = require('./utils');
5
5
 
6
6
  /**
@@ -8,61 +8,33 @@ const { checkMap } = require('./utils');
8
8
  * @param {string} code - The source code to check
9
9
  * @param {Set} polyfills - Set to store detected polyfills
10
10
  */
11
- function detectPolyfills(code, polyfills) {
11
+ const detectPolyfills = (
12
+ code,
13
+ polyfills,
14
+ {
15
+ polyfillPatterns = POLYFILL_PATTERNS,
16
+ importPatterns = IMPORT_PATTERNS
17
+ } = {}) => {
12
18
  if (code.includes('core-js') || code.includes('polyfill')) {
13
- const polyfillPatterns = [
14
- { pattern: /Array\.prototype\.toSorted/, feature: 'ArrayToSorted' },
15
- { pattern: /Array\.prototype\.findLast/, feature: 'ArrayFindLast' },
16
- { pattern: /Array\.prototype\.findLastIndex/, feature: 'ArrayFindLastIndex' },
17
- { pattern: /Array\.prototype\.at/, feature: 'ArrayAt' },
18
- { pattern: /String\.prototype\.replaceAll/, feature: 'StringReplaceAll' },
19
- { pattern: /String\.prototype\.matchAll/, feature: 'StringMatchAll' },
20
- { pattern: /String\.prototype\.at/, feature: 'StringAt' },
21
- { pattern: /Object\.hasOwn/, feature: 'ObjectHasOwn' },
22
- { pattern: /Promise\.any/, feature: 'PromiseAny' },
23
- { pattern: /RegExp\.prototype\.exec/, feature: 'RegExpExec' },
24
- { pattern: /globalThis/, feature: 'GlobalThis' },
25
- ];
26
-
27
19
  for (const { pattern, feature } of polyfillPatterns) {
28
- if (pattern.test(code)) {
29
- polyfills.add(feature);
30
- }
20
+ if (pattern.test(code)) polyfills.add(feature);
31
21
  }
32
22
 
33
23
  if (code.includes('import') && code.includes('core-js')) {
34
- const importPatterns = [
35
- { pattern: /from\s+['"]core-js\/modules\/es\.array\.to-sorted['"]/, feature: 'ArrayToSorted' },
36
- { pattern: /from\s+['"]core-js\/modules\/es\.array\.find-last['"]/, feature: 'ArrayFindLast' },
37
- { pattern: /from\s+['"]core-js\/modules\/es\.array\.find-last-index['"]/, feature: 'ArrayFindLastIndex' },
38
- { pattern: /from\s+['"]core-js\/modules\/es\.array\.at['"]/, feature: 'ArrayAt' },
39
- { pattern: /from\s+['"]core-js\/modules\/es\.string\.replace-all['"]/, feature: 'StringReplaceAll' },
40
- { pattern: /from\s+['"]core-js\/modules\/es\.string\.match-all['"]/, feature: 'StringMatchAll' },
41
- { pattern: /from\s+['"]core-js\/modules\/es\.string\.at['"]/, feature: 'StringAt' },
42
- { pattern: /from\s+['"]core-js\/modules\/es\.object\.has-own['"]/, feature: 'ObjectHasOwn' },
43
- { pattern: /from\s+['"]core-js\/modules\/es\.promise\.any['"]/, feature: 'PromiseAny' },
44
- { pattern: /from\s+['"]core-js\/modules\/es\.regexp\.exec['"]/, feature: 'RegExpExec' },
45
- { pattern: /from\s+['"]core-js\/modules\/es\.global-this['"]/, feature: 'GlobalThis' },
46
- ];
47
-
48
24
  for (const { pattern, feature } of importPatterns) {
49
- if (pattern.test(code)) {
50
- polyfills.add(feature);
51
- }
25
+ if (pattern.test(code)) polyfills.add(feature);
52
26
  }
53
27
  }
54
28
  }
55
29
  }
56
30
 
57
31
  const detectFeatures = (code, ecmaVersion, sourceType, ignoreList = new Set(), options = {}) => {
58
- const { checkForPolyfills } = options;
32
+ const { checkForPolyfills, ast: providedAst } = options;
59
33
 
60
34
  const polyfills = new Set();
61
- if (checkForPolyfills) {
62
- detectPolyfills(code, polyfills);
63
- }
35
+ if (checkForPolyfills) detectPolyfills(code, polyfills);
64
36
 
65
- const ast = acorn.parse(code, {
37
+ const ast = providedAst || acorn.parse(code, {
66
38
  ecmaVersion: 'latest',
67
39
  sourceType,
68
40
  });
package/index.js CHANGED
@@ -11,7 +11,7 @@ let polyfillDetector = null;
11
11
  const pkg = require('./package.json')
12
12
  const { lilconfig } = require('lilconfig');
13
13
  const { JS_VERSIONS } = require('./constants');
14
- const { parseIgnoreList, createLogger, generateBashCompletion, generateZshCompletion } = require('./utils');
14
+ const { parseIgnoreList, createLogger, generateBashCompletion, generateZshCompletion, processBatchedFiles, readFileAsync, parseCode } = require('./utils');
15
15
 
16
16
  program.configureOutput({
17
17
  writeOut: (str) => process.stdout.write(str),
@@ -70,7 +70,7 @@ program
70
70
  .version(pkg.version)
71
71
  .argument(
72
72
  '[ecmaVersion]',
73
- 'ecmaVersion to check files against. Can be: es3, es4, es5, es6/es2015, es7/es2016, es8/es2017, es9/es2018, es10/es2019, es11/es2020, es12/es2021, es13/es2022, es14/es2023, checkBrowser',
73
+ 'ecmaVersion to check files against. Can be: es3, es4, es5, es6/es2015, es7/es2016, es8/es2017, es9/es2018, es10/es2019, es11/es2020, es12/es2021, es13/es2022, es14/es2023, es15/es2024, es16/es2025, checkBrowser',
74
74
  )
75
75
  .argument('[files...]', 'a glob of files to to test the EcmaScript version against')
76
76
  .option('--module', 'use ES modules')
@@ -98,6 +98,7 @@ program
98
98
  .option('--browserslistPath <path>', 'path to custom browserslist configuration')
99
99
  .option('--browserslistEnv <env>', 'browserslist environment to use')
100
100
  .option('--config <path>', 'path to custom .escheckrc config file')
101
+ .option('--batchSize <number>', 'number of files to process concurrently (0 for unlimited)', '0')
101
102
 
102
103
  async function loadConfig(customConfigPath) {
103
104
  const logger = createLogger();
@@ -178,6 +179,7 @@ program
178
179
  browserslistQuery: options.browserslistQuery !== undefined ? options.browserslistQuery : baseConfig.browserslistQuery,
179
180
  browserslistPath: options.browserslistPath !== undefined ? options.browserslistPath : baseConfig.browserslistPath,
180
181
  browserslistEnv: options.browserslistEnv !== undefined ? options.browserslistEnv : baseConfig.browserslistEnv,
182
+ batchSize: options.batchSize !== undefined ? options.batchSize : baseConfig.batchSize,
181
183
  };
182
184
 
183
185
  if (ecmaVersionArg !== undefined) {
@@ -332,6 +334,14 @@ async function runChecks(configs, logger) {
332
334
  case 'es2023':
333
335
  ecmaVersion = '14'
334
336
  break
337
+ case 'es15':
338
+ case 'es2024':
339
+ ecmaVersion = '15'
340
+ break
341
+ case 'es16':
342
+ case 'es2025':
343
+ ecmaVersion = '16'
344
+ break
335
345
  default:
336
346
  logger.error('Invalid ecmaScript version, please pass a valid version, use --help for help')
337
347
  process.exit(1)
@@ -359,20 +369,22 @@ async function runChecks(configs, logger) {
359
369
  }
360
370
  }
361
371
 
362
- const expandedPathsToIgnore = pathsToIgnore.reduce((result, path) =>
363
- path.includes('*') ? result.concat(glob.sync(path, globOpts)) : result.concat(path)
364
- , [])
365
-
366
- const filterForIgnore = (globbedFiles) => {
367
- if (expandedPathsToIgnore && expandedPathsToIgnore.length > 0) {
368
- return globbedFiles.filter(
369
- (filePath) => !expandedPathsToIgnore.some((ignoreValue) => filePath.includes(ignoreValue))
370
- );
371
- }
372
- return globbedFiles;
372
+ let expandedPathsToIgnore = [];
373
+ if (pathsToIgnore.length > 0) {
374
+ expandedPathsToIgnore = pathsToIgnore.reduce((result, path) => {
375
+ if (path.includes('*')) {
376
+ return result.concat(glob.sync(path, globOpts));
377
+ }
378
+ return result.concat(path);
379
+ }, []);
373
380
  }
374
381
 
375
- const filteredFiles = filterForIgnore(allMatchedFiles)
382
+ let filteredFiles = allMatchedFiles;
383
+ if (expandedPathsToIgnore.length > 0) {
384
+ filteredFiles = allMatchedFiles.filter((filePath) => {
385
+ return !expandedPathsToIgnore.some((ignoreValue) => filePath.includes(ignoreValue));
386
+ });
387
+ }
376
388
 
377
389
  const ignoreList = parseIgnoreList(config);
378
390
 
@@ -380,27 +392,27 @@ async function runChecks(configs, logger) {
380
392
  logger.debug('ES-Check: ignoring features:', Array.from(ignoreList).join(', '));
381
393
  }
382
394
 
383
- filteredFiles.forEach((file) => {
384
- const code = fs.readFileSync(file, 'utf8')
395
+ const batchSize = parseInt(config.batchSize || '0', 10);
396
+
397
+ const processFile = async (file) => {
398
+ const { content: code, error: readError } = await readFileAsync(file, fs);
399
+ if (readError) {
400
+ return readError;
401
+ }
402
+
385
403
  if (logger.isLevelEnabled('debug')) {
386
404
  logger.debug(`ES-Check: checking ${file}`)
387
405
  }
388
- try {
389
- acorn.parse(code, acornOpts)
390
- } catch (err) {
406
+
407
+ const { ast, error: parseError } = parseCode(code, acornOpts, acorn, file);
408
+ if (parseError) {
391
409
  if (logger.isLevelEnabled('debug')) {
392
- logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${err}`)
410
+ logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${parseError.err}`)
393
411
  }
394
- const errorObj = {
395
- err,
396
- stack: err.stack,
397
- file,
398
- }
399
- errArray.push(errorObj);
400
- return;
412
+ return parseError;
401
413
  }
402
414
 
403
- if (!checkFeatures) return;
415
+ if (!checkFeatures) return null;
404
416
  const parseSourceType = acornOpts.sourceType || 'script';
405
417
  const esVersion = parseInt(ecmaVersion, 10);
406
418
 
@@ -408,7 +420,8 @@ async function runChecks(configs, logger) {
408
420
  code,
409
421
  esVersion,
410
422
  parseSourceType,
411
- ignoreList
423
+ ignoreList,
424
+ { ast, checkForPolyfills }
412
425
  );
413
426
 
414
427
  if (logger.isLevelEnabled('debug')) {
@@ -434,13 +447,18 @@ async function runChecks(configs, logger) {
434
447
  const isSupported = filteredUnsupportedFeatures.length === 0;
435
448
  if (!isSupported) {
436
449
  const error = new Error(`Unsupported features used: ${filteredUnsupportedFeatures.join(', ')} but your target is ES${ecmaVersion}.`);
437
- errArray.push({
450
+ return {
438
451
  err: error,
439
452
  file,
440
453
  stack: error.stack
441
- });
454
+ };
442
455
  }
443
- })
456
+ return null;
457
+ };
458
+
459
+ const results = await processBatchedFiles(filteredFiles, processFile, batchSize);
460
+ const errors = results.filter(result => result !== null);
461
+ errArray.push(...errors);
444
462
 
445
463
  if (errArray.length > 0) {
446
464
  logger.error(`ES-Check: there were ${errArray.length} ES version matching errors.`)
@@ -467,4 +485,11 @@ async function runChecks(configs, logger) {
467
485
  }
468
486
  }
469
487
 
470
- program.parse()
488
+ if (require.main === module) {
489
+ program.parse()
490
+ }
491
+
492
+ module.exports = {
493
+ runChecks,
494
+ loadConfig
495
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-check",
3
- "version": "9.1.4",
3
+ "version": "9.2.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",
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "commit": "git-cz",
20
20
  "commit-msg": "commitlint --edit $1",
21
+ "dev": "pnpm site:dev",
21
22
  "lint": "eslint index.js --fix",
22
23
  "lint:ci": "eslint index.js utils.js detectFeatures.js constants.js browserslist.js polyfillDetector.js",
23
24
  "pre-commit": "pnpm lint && pnpm test",
@@ -28,7 +29,11 @@
28
29
  "setup": "pnpm install --reporter=silent",
29
30
  "test": "nyc mocha test.js utils.test.js browserslist.test.js polyfillDetector.test.js detectFeatures.test.js --timeout 10s",
30
31
  "update": "codependence --update",
31
- "benchmark": "./benchmarks/run-benchmarks.sh"
32
+ "benchmark": "./benchmarks/run-benchmarks.sh",
33
+ "site:dev": "pnpm --filter es-check-docs run dev",
34
+ "site:build": "pnpm --filter es-check-docs run build",
35
+ "site:preview": "pnpm --filter es-check-docs run preview",
36
+ "site:install": "pnpm --filter es-check-docs install"
32
37
  },
33
38
  "repository": {
34
39
  "type": "git",
@@ -52,19 +57,19 @@
52
57
  "codependence": "^0.3.1",
53
58
  "commitizen": "4.3.1",
54
59
  "conventional-changelog-cli": "^5.0.0",
55
- "eslint": "9.28.0",
56
- "eslint-config-prettier": "10.1.5",
60
+ "eslint": "9.32.0",
61
+ "eslint-config-prettier": "10.1.8",
57
62
  "eslint-plugin-es5": "^1.5.0",
58
63
  "husky": "9.1.7",
59
64
  "is-ci": "^3.0.1",
60
- "mocha": "11.5.0",
65
+ "mocha": "11.7.1",
61
66
  "nyc": "^17.1.0",
62
67
  "path-exists-cli": "^2.0.0",
63
- "prettier": "3.5.3",
64
- "release-it": "19.0.3"
68
+ "prettier": "3.6.2",
69
+ "release-it": "19.0.4"
65
70
  },
66
71
  "dependencies": {
67
- "acorn": "8.14.1",
72
+ "acorn": "8.15.0",
68
73
  "acorn-walk": "^8.3.4",
69
74
  "browserslist": "^4.23.3",
70
75
  "commander": "14.0.0",
@@ -1,77 +1,47 @@
1
1
  const { FEATURE_TO_POLYFILL_MAP } = require('./constants');
2
2
 
3
3
  /**
4
- * Detects polyfills in the code and returns a set of polyfilled feature names
5
- * @param {string} code - The source code to check
6
- * @param {Object} logger - Winston logger instance for debug output
7
- * @returns {Set<string>} - Set of polyfilled feature names
4
+ * Detects polyfills in the code and returns a set of polyfilled feature names.
5
+ *
6
+ * @param {string} code - The source code to check.
7
+ * @param {object} logger - A required logger instance.
8
+ * @param {object} [featureMap=FEATURE_TO_POLYFILL_MAP] - The map of features.
9
+ * @returns {Set<string>} - A set of polyfilled feature names.
8
10
  */
9
- function detectPolyfills(code, logger) {
10
- const polyfills = new Set();
11
-
12
- if (!code) {
13
- return polyfills;
14
- }
15
-
16
- if (code.includes('import') && code.includes('core-js')) {
17
- if (code.includes('core-js/modules/es.array.to-sorted')) {
18
- polyfills.add('ArrayToSorted');
19
- }
20
- if (code.includes('core-js/modules/es.object.has-own')) {
21
- polyfills.add('ObjectHasOwn');
22
- }
23
- if (code.includes('core-js/modules/es.string.replace-all')) {
24
- polyfills.add('StringReplaceAll');
11
+ function detectPolyfills(code, logger, featureMap = FEATURE_TO_POLYFILL_MAP) {
12
+ const polyfills = new Set();
13
+ if (!code || !featureMap) return polyfills;
14
+
15
+ // Since logger is required, we can use it without checking for its existence.
16
+ // The optional chaining (?.) is still good practice for properties like isLevelEnabled.
17
+ if (logger?.isLevelEnabled?.('debug')) {
18
+ // We can log at the beginning if needed, or at the end.
25
19
  }
26
20
 
27
- if (polyfills.size > 0) {
28
- return polyfills;
29
- }
21
+ const polyfillFeatures = Object.entries(featureMap);
22
+ polyfillFeatures.forEach(([feature, patterns]) => {
23
+ const isPolyfilled = patterns.some(pattern => pattern.test(code));
24
+ if (isPolyfilled) polyfills.add(feature);
25
+ });
30
26
 
31
- for (const [featureName, patterns] of Object.entries(FEATURE_TO_POLYFILL_MAP)) {
32
- for (const pattern of patterns) {
33
- if (pattern.test(code)) {
34
- polyfills.add(featureName);
35
- break;
36
- }
37
- }
27
+ if (logger?.isLevelEnabled?.('debug')) {
28
+ const hasPolyfills = polyfills.size > 0;
29
+ if (hasPolyfills) logger.debug(`ES-Check: Detected polyfills: ${Array.from(polyfills).join(', ')}`);
30
+ else logger.debug('ES-Check: No polyfills detected.');
38
31
  }
39
- }
40
32
 
41
- if (polyfills.size === 0 && (code.includes('polyfill') || code.includes('Array.prototype') || code.includes('Object.') || code.includes('String.prototype'))) {
42
- } else if (polyfills.size > 0) {
43
33
  return polyfills;
44
- } else if (!code.includes('core-js') && !code.includes('polyfill') && !code.includes('Array.prototype')) {
45
- return polyfills;
46
- }
47
-
48
- for (const [featureName, patterns] of Object.entries(FEATURE_TO_POLYFILL_MAP)) {
49
- for (const pattern of patterns) {
50
- if (pattern.test(code)) {
51
- polyfills.add(featureName);
52
- break;
53
- }
54
- }
55
- }
56
-
57
- if (logger?.isLevelEnabled?.('debug') && polyfills.size > 0) {
58
- logger.debug(`ES-Check: Detected polyfills: ${Array.from(polyfills).join(', ')}`);
59
- }
60
-
61
- return polyfills;
62
34
  }
63
35
 
64
36
  /**
65
- * Filters unsupported features by removing those that have been polyfilled
37
+ * Filters unsupported features by removing those that have been polyfilled.
66
38
  * @param {Array<string>} unsupportedFeatures - List of unsupported features
67
39
  * @param {Set<string>} polyfills - Set of polyfilled feature names
68
40
  * @returns {Array<string>} - Filtered list of unsupported features
69
41
  */
70
- function filterPolyfilled(unsupportedFeatures, polyfills) {
71
- if (!polyfills || polyfills.size === 0) {
72
- return unsupportedFeatures;
73
- }
74
-
42
+ const filterPolyfilled = (unsupportedFeatures, polyfills) => {
43
+ const hasPolyfills = polyfills && polyfills.size > 0;
44
+ if (!hasPolyfills) return unsupportedFeatures;
75
45
  return unsupportedFeatures.filter(feature => !polyfills.has(feature));
76
46
  }
77
47
 
package/utils.js CHANGED
@@ -113,6 +113,12 @@ const checkMap = {
113
113
  node.callee.property.type === 'Identifier' &&
114
114
  node.callee.property.name === astInfo.property
115
115
  );
116
+ } else if (astInfo.property) {
117
+ // Check for method calls with any object when only property is specified
118
+ return (
119
+ node.callee.property.type === 'Identifier' &&
120
+ node.callee.property.name === astInfo.property
121
+ );
116
122
  }
117
123
 
118
124
  return false;
@@ -180,7 +186,7 @@ function createLogger(options = {}) {
180
186
  level,
181
187
  stderrLevels: ['error', 'warn'],
182
188
  format: winston.format.combine(
183
- ...(supportsColor.stdout || !noColor ? [winston.format.colorize()] : []),
189
+ ...(supportsColor.stdout && !noColor ? [winston.format.colorize()] : []),
184
190
  winston.format.simple(),
185
191
  ),
186
192
  })
@@ -217,7 +223,7 @@ _${cmdName.replace(/-/g, '_')}_completion() {
217
223
  opts="${optsStr}"
218
224
 
219
225
  # ES versions
220
- es_versions="es3 es5 es6 es2015 es7 es2016 es8 es2017 es9 es2018 es10 es2019 es11 es2020 es12 es2021 es13 es2022 es14 es2023"
226
+ es_versions="es3 es5 es6 es2015 es7 es2016 es8 es2017 es9 es2018 es10 es2019 es11 es2020 es12 es2021 es13 es2022 es14 es2023 es15 es2024 es16 es2025"
221
227
 
222
228
  # Handle special cases based on previous argument
223
229
  case "\$prev" in
@@ -288,6 +294,10 @@ _${cmdName.replace(/-/g, '_')}() {
288
294
  "es2022:ECMAScript 2022"
289
295
  "es14:ECMAScript 2023"
290
296
  "es2023:ECMAScript 2023"
297
+ "es15:ECMAScript 2024"
298
+ "es2024:ECMAScript 2024"
299
+ "es16:ECMAScript 2025"
300
+ "es2025:ECMAScript 2025"
291
301
  )
292
302
 
293
303
  # Commands
@@ -328,6 +338,75 @@ _${cmdName.replace(/-/g, '_')}
328
338
  `;
329
339
  }
330
340
 
341
+ /**
342
+ * Process files in batches for better performance
343
+ * @param {string[]} files - Array of file paths to process
344
+ * @param {Function} processor - Async function to process each file
345
+ * @param {number} batchSize - Number of files to process concurrently (0 for unlimited)
346
+ * @returns {Promise<Array>} Array of results from processing all files
347
+ */
348
+ async function processBatchedFiles(files, processor, batchSize = 0) {
349
+ if (batchSize <= 0) {
350
+ return Promise.all(files.map(processor));
351
+ }
352
+
353
+ const results = [];
354
+
355
+ for (let i = 0; i < files.length; i += batchSize) {
356
+ const batch = files.slice(i, i + batchSize);
357
+ const batchResults = await Promise.all(batch.map(processor));
358
+ results.push(...batchResults);
359
+ }
360
+
361
+ return results;
362
+ }
363
+
364
+ /**
365
+ * Read file asynchronously with error handling
366
+ * @param {string} file - File path to read
367
+ * @param {Object} fs - File system module
368
+ * @returns {Promise<{content: string, error: null} | {content: null, error: Object}>}
369
+ */
370
+ async function readFileAsync(file, fs) {
371
+ try {
372
+ const content = await fs.promises.readFile(file, 'utf8');
373
+ return { content, error: null };
374
+ } catch (err) {
375
+ return {
376
+ content: null,
377
+ error: {
378
+ err,
379
+ file,
380
+ stack: err.stack
381
+ }
382
+ };
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Parse code with acorn and handle errors
388
+ * @param {string} code - Code to parse
389
+ * @param {Object} acornOpts - Acorn parsing options
390
+ * @param {Object} acorn - Acorn module
391
+ * @param {string} file - File path for error reporting
392
+ * @returns {{ast: Object, error: null} | {ast: null, error: Object}}
393
+ */
394
+ function parseCode(code, acornOpts, acorn, file) {
395
+ try {
396
+ const ast = acorn.parse(code, acornOpts);
397
+ return { ast, error: null };
398
+ } catch (err) {
399
+ return {
400
+ ast: null,
401
+ error: {
402
+ err,
403
+ stack: err.stack,
404
+ file
405
+ }
406
+ };
407
+ }
408
+ }
409
+
331
410
  module.exports = {
332
411
  parseIgnoreList,
333
412
  checkVarKindMatch,
@@ -337,5 +416,8 @@ module.exports = {
337
416
  checkMap,
338
417
  createLogger,
339
418
  generateBashCompletion,
340
- generateZshCompletion
419
+ generateZshCompletion,
420
+ processBatchedFiles,
421
+ readFileAsync,
422
+ parseCode
341
423
  };