@utoo/lint 0.2.2 → 0.2.4

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
@@ -41,6 +41,11 @@ Use machine-readable output:
41
41
  pnpm exec utoo-lint --format=json src
42
42
  ```
43
43
 
44
+ The default terminal output groups diagnostics by file, aligns their location
45
+ and severity, and summarizes errors and warnings separately. Color is detected
46
+ automatically; pass `--color` to force it or `--no-color` to disable it. JSON
47
+ output remains stable for editor and CI integrations.
48
+
44
49
  ## Autofix
45
50
 
46
51
  Apply safe fixes from all enabled rules that support autofix:
@@ -64,6 +69,21 @@ lists autofix coverage by rule.
64
69
 
65
70
  ## Configuration
66
71
 
72
+ `utoo-lint` supports two canonical project config formats:
73
+
74
+ | Config | Use it when | Raw native binary |
75
+ | --- | --- | --- |
76
+ | `utlint.config.ts` | You want typed authoring, imports, or computed values. | No |
77
+ | `utlint.config.json` | You want a static, runtime-independent config. | Yes |
78
+
79
+ The npm CLI discovers either format automatically. It can also select one
80
+ explicitly:
81
+
82
+ ```bash
83
+ pnpm exec utoo-lint --config=utlint.config.ts src
84
+ pnpm exec utoo-lint --config=utlint.config.json src
85
+ ```
86
+
67
87
  For a static config readable by both the npm CLI and raw native binary, create
68
88
  `utlint.config.json`:
69
89
 
package/index.cjs CHANGED
@@ -484,7 +484,7 @@ class UtooLint {
484
484
  }
485
485
 
486
486
  async lintFiles(patterns, options = {}) {
487
- const mergedOptions = mergeLintOptions(this.options, options);
487
+ const mergedOptions = withConfigCache(mergeLintOptions(this.options, options));
488
488
  const customRuleConfig = [mergedOptions.baseConfig, mergedOptions.overrideConfig].filter(Boolean);
489
489
  const customRuleFilterMap = customRuleMapForConfig(customRuleConfig, new Map());
490
490
  const nativeOptions = lintOptionsWithoutCustomRules(mergedOptions, customRuleFilterMap);
@@ -504,7 +504,7 @@ class UtooLint {
504
504
  throw new TypeError("code must be a string");
505
505
  }
506
506
 
507
- const mergedOptions = mergeLintOptions(this.options, options);
507
+ const mergedOptions = withConfigCache(mergeLintOptions(this.options, options));
508
508
  const filePath = normalizeESLintFilePath(textFilePathForOptions(options, "<text>"), mergedOptions.cwd);
509
509
  const customRuleConfig = [mergedOptions.baseConfig, mergedOptions.overrideConfig].filter(Boolean);
510
510
  const customRuleMap = customRuleMapForConfig(customRuleConfig, new Map(), filePath, mergedOptions.cwd);
@@ -528,7 +528,7 @@ class UtooLint {
528
528
  }
529
529
 
530
530
  async calculateConfigForFile(filePath) {
531
- return publicCalculatedConfig(eslintConstructorOptions(this.options), filePath);
531
+ return publicCalculatedConfig(withConfigCache(eslintConstructorOptions(this.options)), filePath);
532
532
  }
533
533
 
534
534
  async findConfigFile(filePath) {
@@ -610,7 +610,7 @@ class CLIEngine {
610
610
  }
611
611
 
612
612
  executeOnFiles(patterns) {
613
- const mergedOptions = eslintConstructorOptions(this.options);
613
+ const mergedOptions = withConfigCache(eslintConstructorOptions(this.options));
614
614
  const report = lintFiles(patterns, mergedOptions);
615
615
  throwOnUnmatchedPatternDiagnostics(report, mergedOptions);
616
616
  const results = maybeFilterQuietResults(reportToESLintResults(report, {
@@ -630,10 +630,10 @@ class CLIEngine {
630
630
  typeof filePathOrOptions === "object" && filePathOrOptions !== null
631
631
  ? filePathOrOptions.filePath ?? filePathOrOptions.filename ?? "input.js"
632
632
  : filePathOrOptions;
633
- const mergedOptions = {
633
+ const mergedOptions = withConfigCache({
634
634
  ...eslintConstructorOptions(this.options),
635
635
  ...textOptions
636
- };
636
+ });
637
637
  const report = lintText(code, {
638
638
  ...mergedOptions,
639
639
  filePath
@@ -670,7 +670,7 @@ class CLIEngine {
670
670
  }
671
671
 
672
672
  getConfigForFile(filePath) {
673
- return publicCalculatedConfig(eslintConstructorOptions(this.options), filePath);
673
+ return publicCalculatedConfig(withConfigCache(eslintConstructorOptions(this.options)), filePath);
674
674
  }
675
675
  }
676
676
 
@@ -747,6 +747,14 @@ function parseUtooLintCliArgs(args = []) {
747
747
  passthroughArgs.push(arg);
748
748
  continue;
749
749
  }
750
+ if (arg === "--color") {
751
+ options.color = true;
752
+ continue;
753
+ }
754
+ if (arg === "--no-color") {
755
+ options.color = false;
756
+ continue;
757
+ }
750
758
  if (arg.startsWith("-")) {
751
759
  passthroughArgs.push(arg);
752
760
  continue;
@@ -812,12 +820,12 @@ function runCli(args = [], options = {}) {
812
820
  return run(parsed.passthroughArgs, options);
813
821
  }
814
822
 
815
- const cliOptions = {
823
+ const cliOptions = withConfigCache({
816
824
  ...options,
817
825
  ...parsed.options,
818
826
  extraArgs: parsed.passthroughArgs,
819
827
  preserveNativeDefaults: Boolean(parsed.options.noConfig)
820
- };
828
+ });
821
829
  const targets = parsed.targets.length > 0 ? parsed.targets : defaultLintTargets(cliOptions);
822
830
  const report = lintFiles(targets, cliOptions);
823
831
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, cliOptions);
@@ -826,7 +834,7 @@ function runCli(args = [], options = {}) {
826
834
  if (cliOptions.format === "json" || parsed.passthroughArgs.includes("--json")) {
827
835
  return completedRunResult(status, `${JSON.stringify(publicCliReport(report))}\n`, report.stderr ?? "", options);
828
836
  }
829
- return completedRunResult(status, "", `${report.stderr ?? ""}${formatNativeTextReport(report)}`, options);
837
+ return completedRunResult(status, "", `${report.stderr ?? ""}${formatNativeTextReport(report, cliOptions)}`, options);
830
838
  }
831
839
 
832
840
  function publicCliReport(report) {
@@ -838,18 +846,82 @@ function publicCliReport(report) {
838
846
  };
839
847
  }
840
848
 
841
- function formatNativeTextReport(report) {
849
+ function formatNativeTextReport(report, options = {}) {
850
+ const diagnostics = report.diagnostics ?? [];
851
+ const files = report.files ?? 0;
852
+ const color = shouldUseColor(options);
853
+
854
+ if (diagnostics.length === 0) {
855
+ return `${paint(color, "\u001b[32m", "✓")} ${pluralize(files, "file")} checked, no problems found\n`;
856
+ }
857
+
842
858
  const lines = [];
843
- for (const diagnostic of report.diagnostics ?? []) {
844
- const rule = diagnostic.ruleId ? ` [${diagnostic.ruleId}]` : "";
859
+ const locationWidth = diagnostics.reduce(
860
+ (width, diagnostic) => Math.max(width, diagnosticLocation(diagnostic).length),
861
+ 0
862
+ );
863
+ let currentFile;
864
+
865
+ for (const diagnostic of diagnostics) {
866
+ const filePath = diagnostic.filePath ?? "<unknown>";
867
+ if (filePath !== currentFile) {
868
+ if (lines.length > 0) lines.push("");
869
+ lines.push(paint(color, "\u001b[1m", filePath));
870
+ currentFile = filePath;
871
+ }
872
+
873
+ const location = diagnosticLocation(diagnostic).padStart(locationWidth);
874
+ const severity = diagnostic.severity === "warning" || diagnostic.severity === 1 ? "warning" : "error";
875
+ const severityColor = severity === "error" ? "\u001b[31m" : "\u001b[33m";
876
+ const message = String(diagnostic.message ?? "").replace(/\s*[\r\n]+\s*/g, " ");
877
+ const rule = diagnostic.ruleId ? ` ${paint(color, "\u001b[2m", diagnostic.ruleId)}` : "";
845
878
  lines.push(
846
- `${diagnostic.filePath}:${diagnostic.line ?? 0}:${diagnostic.column ?? 0}: ${diagnostic.severity ?? "error"}: ${diagnostic.message}${rule}`
879
+ ` ${paint(color, "\u001b[2m", location)} ${paint(color, severityColor, severity.padEnd(7))} ${message}${rule}`
847
880
  );
848
881
  }
849
- lines.push(`${report.files ?? 0} file(s) checked, ${(report.diagnostics ?? []).length} diagnostic(s)`);
882
+
883
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === 2).length;
884
+ const warnings = diagnostics.length - errors;
885
+ const markerColor = errors > 0 ? "\u001b[31m" : "\u001b[33m";
886
+ lines.push("");
887
+ lines.push(
888
+ `${paint(color, markerColor, "✖")} ${pluralize(diagnostics.length, "problem")} (${pluralize(errors, "error")}, ${pluralize(warnings, "warning")})`
889
+ );
890
+
891
+ const fixable = diagnostics.filter(diagnosticIsFixable).length;
892
+ const fixRequested = options.fix || options.extraArgs?.includes("--fix") || options.extraArgs?.includes("--fix-dry-run");
893
+ if (fixable > 0 && !fixRequested) {
894
+ lines.push(` ${pluralize(fixable, "problem")} potentially fixable with the \`--fix\` option.`);
895
+ }
896
+ lines.push(` ${pluralize(files, "file")} checked`);
850
897
  return `${lines.join("\n")}\n`;
851
898
  }
852
899
 
900
+ function diagnosticLocation(diagnostic) {
901
+ return `${diagnostic.line ?? 0}:${diagnostic.column ?? 0}`;
902
+ }
903
+
904
+ function diagnosticIsFixable(diagnostic) {
905
+ return Boolean(diagnostic.fix) || (Array.isArray(diagnostic.fixes) && diagnostic.fixes.length > 0);
906
+ }
907
+
908
+ function pluralize(count, noun) {
909
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
910
+ }
911
+
912
+ function paint(enabled, code, text) {
913
+ return enabled ? `${code}${text}\u001b[0m` : text;
914
+ }
915
+
916
+ function shouldUseColor(options = {}) {
917
+ if (options.color === true) return true;
918
+ if (options.color === false) return false;
919
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
920
+ if (env.NO_COLOR !== undefined || env.NODE_DISABLE_COLORS !== undefined) return false;
921
+ if ((env.FORCE_COLOR && env.FORCE_COLOR !== "0") || (env.CLICOLOR_FORCE && env.CLICOLOR_FORCE !== "0")) return true;
922
+ return Boolean(process.stderr.isTTY);
923
+ }
924
+
853
925
  function runFishlint(args = [], options = {}) {
854
926
  const cliArgs = normalizeStringArray(args, "args");
855
927
  const env = options.env ? { ...process.env, ...options.env } : { ...process.env };
@@ -1063,6 +1135,7 @@ function booleanFlagValue(arg) {
1063
1135
  }
1064
1136
 
1065
1137
  function lintFiles(paths, options = {}) {
1138
+ options = withConfigCache(options);
1066
1139
  const targets = lintTargetsForInput(paths, options);
1067
1140
  const ignoredDiagnostics = ignoredLintPathDiagnostics(targets, options);
1068
1141
  const lintPaths = filteredLintPaths(targets, options);
@@ -1241,6 +1314,7 @@ function mergeNativeReports(reports) {
1241
1314
  }
1242
1315
 
1243
1316
  function lintText(code, options = {}) {
1317
+ options = withConfigCache(options);
1244
1318
  if (typeof code !== "string") {
1245
1319
  throw new TypeError("code must be a string");
1246
1320
  }
@@ -1307,7 +1381,7 @@ function buildNativeLintArgs(paths, options) {
1307
1381
  if (!options.rules && !options.forceMaterializedConfig && (options.config || options.baseConfig || options.overrideConfig)) {
1308
1382
  const configForRuleSelection = options.noConfig
1309
1383
  ? undefined
1310
- : options.config ? readConfig(options.config, options.cwd) : undefined;
1384
+ : options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
1311
1385
  const enabledRules = enabledRuleNamesFromConfigs(
1312
1386
  configForRuleSelection,
1313
1387
  options.baseConfig,
@@ -1353,6 +1427,12 @@ function mergeLintOptions(base, override) {
1353
1427
  };
1354
1428
  }
1355
1429
 
1430
+ function withConfigCache(options) {
1431
+ // Executable configs are otherwise re-evaluated at every per-file lookup.
1432
+ // Keep the cache on the options object so one public invocation shares it.
1433
+ return options.configCache ? options : { ...options, configCache: new Map() };
1434
+ }
1435
+
1356
1436
  function eslintConstructorOptions(options) {
1357
1437
  const mapped = {
1358
1438
  cwd: options.cwd,
@@ -1525,7 +1605,7 @@ function rulesFromFileConfig(options, filePath) {
1525
1605
  return {};
1526
1606
  }
1527
1607
 
1528
- const config = readConfig(configPath, options.cwd);
1608
+ const config = readConfig(configPath, options.cwd, options.configCache);
1529
1609
  return rulesFromConfig(config, filePath, dirname(configPath));
1530
1610
  }
1531
1611
 
@@ -1539,7 +1619,7 @@ function configDataFromFileConfig(options, filePath) {
1539
1619
  return {};
1540
1620
  }
1541
1621
 
1542
- const config = readConfig(configPath, options.cwd);
1622
+ const config = readConfig(configPath, options.cwd, options.configCache);
1543
1623
  return configDataFromConfig(config, filePath, dirname(configPath));
1544
1624
  }
1545
1625
 
@@ -1560,7 +1640,7 @@ function defaultLintTargets(options = {}) {
1560
1640
  return ["."];
1561
1641
  }
1562
1642
 
1563
- const targets = filePatternsFromConfig(readConfig(configPath, options.cwd));
1643
+ const targets = filePatternsFromConfig(readConfig(configPath, options.cwd, options.configCache));
1564
1644
  return targets.length > 0
1565
1645
  ? targets.map((target) => resolveConfigPattern(target, dirname(configPath)))
1566
1646
  : ["."];
@@ -1652,7 +1732,9 @@ function configPathFromDirectory(directory) {
1652
1732
  function withTemporaryConfig(options, callback) {
1653
1733
  const fileConfigPath = !options.noConfig ? configPathForOptions(options) : undefined;
1654
1734
  const shouldMaterializeFileConfig = Boolean(fileConfigPath);
1655
- const fileConfig = shouldMaterializeFileConfig ? readConfig(fileConfigPath, options.cwd) : undefined;
1735
+ const fileConfig = shouldMaterializeFileConfig
1736
+ ? readConfig(fileConfigPath, options.cwd, options.configCache)
1737
+ : undefined;
1656
1738
  const configs = [options.baseConfig, fileConfig, options.overrideConfig];
1657
1739
  const rules = shouldMaterializeFileConfig
1658
1740
  ? materializedRulesFromConfigs(...configs)
@@ -4012,7 +4094,7 @@ function ignorePatternsFromFileConfig(options, filePath) {
4012
4094
  }
4013
4095
 
4014
4096
  const cwd = options.cwd ?? process.cwd();
4015
- return ignorePatternsFromConfig(readConfig(configPath, options.cwd)).map((pattern) =>
4097
+ return ignorePatternsFromConfig(readConfig(configPath, options.cwd, options.configCache)).map((pattern) =>
4016
4098
  rebaseConfigPattern(pattern, dirname(configPath), cwd)
4017
4099
  );
4018
4100
  }
package/index.d.ts CHANGED
@@ -88,6 +88,7 @@ export interface LintOptions {
88
88
  threads?: number;
89
89
  rules?: string[] | string;
90
90
  format?: "text" | "json" | string;
91
+ color?: boolean;
91
92
  config?: string;
92
93
  configFile?: string;
93
94
  noConfig?: boolean;
package/index.js CHANGED
@@ -487,7 +487,7 @@ export class UtooLint {
487
487
  }
488
488
 
489
489
  async lintFiles(patterns, options = {}) {
490
- const mergedOptions = mergeLintOptions(this.options, options);
490
+ const mergedOptions = withConfigCache(mergeLintOptions(this.options, options));
491
491
  const customRuleConfig = [mergedOptions.baseConfig, mergedOptions.overrideConfig].filter(Boolean);
492
492
  const customRuleFilterMap = customRuleMapForConfig(customRuleConfig, new Map());
493
493
  const nativeOptions = lintOptionsWithoutCustomRules(mergedOptions, customRuleFilterMap);
@@ -507,7 +507,7 @@ export class UtooLint {
507
507
  throw new TypeError("code must be a string");
508
508
  }
509
509
 
510
- const mergedOptions = mergeLintOptions(this.options, options);
510
+ const mergedOptions = withConfigCache(mergeLintOptions(this.options, options));
511
511
  const filePath = normalizeESLintFilePath(textFilePathForOptions(options, "<text>"), mergedOptions.cwd);
512
512
  const customRuleConfig = [mergedOptions.baseConfig, mergedOptions.overrideConfig].filter(Boolean);
513
513
  const customRuleMap = customRuleMapForConfig(customRuleConfig, new Map(), filePath, mergedOptions.cwd);
@@ -531,7 +531,7 @@ export class UtooLint {
531
531
  }
532
532
 
533
533
  async calculateConfigForFile(filePath) {
534
- return publicCalculatedConfig(eslintConstructorOptions(this.options), filePath);
534
+ return publicCalculatedConfig(withConfigCache(eslintConstructorOptions(this.options)), filePath);
535
535
  }
536
536
 
537
537
  async findConfigFile(filePath) {
@@ -2446,7 +2446,7 @@ export class CLIEngine {
2446
2446
  }
2447
2447
 
2448
2448
  executeOnFiles(patterns) {
2449
- const mergedOptions = eslintConstructorOptions(this.options);
2449
+ const mergedOptions = withConfigCache(eslintConstructorOptions(this.options));
2450
2450
  const report = lintFiles(patterns, mergedOptions);
2451
2451
  throwOnUnmatchedPatternDiagnostics(report, mergedOptions);
2452
2452
  const results = maybeFilterQuietResults(reportToESLintResults(report, {
@@ -2466,10 +2466,10 @@ export class CLIEngine {
2466
2466
  typeof filePathOrOptions === "object" && filePathOrOptions !== null
2467
2467
  ? filePathOrOptions.filePath ?? filePathOrOptions.filename ?? "input.js"
2468
2468
  : filePathOrOptions;
2469
- const mergedOptions = {
2469
+ const mergedOptions = withConfigCache({
2470
2470
  ...eslintConstructorOptions(this.options),
2471
2471
  ...textOptions
2472
- };
2472
+ });
2473
2473
  const report = lintText(code, {
2474
2474
  ...mergedOptions,
2475
2475
  filePath
@@ -2506,7 +2506,7 @@ export class CLIEngine {
2506
2506
  }
2507
2507
 
2508
2508
  getConfigForFile(filePath) {
2509
- return publicCalculatedConfig(eslintConstructorOptions(this.options), filePath);
2509
+ return publicCalculatedConfig(withConfigCache(eslintConstructorOptions(this.options)), filePath);
2510
2510
  }
2511
2511
  }
2512
2512
 
@@ -2583,6 +2583,14 @@ function parseUtooLintCliArgs(args = []) {
2583
2583
  passthroughArgs.push(arg);
2584
2584
  continue;
2585
2585
  }
2586
+ if (arg === "--color") {
2587
+ options.color = true;
2588
+ continue;
2589
+ }
2590
+ if (arg === "--no-color") {
2591
+ options.color = false;
2592
+ continue;
2593
+ }
2586
2594
  if (arg.startsWith("-")) {
2587
2595
  passthroughArgs.push(arg);
2588
2596
  continue;
@@ -2648,12 +2656,12 @@ export function runCli(args = [], options = {}) {
2648
2656
  return run(parsed.passthroughArgs, options);
2649
2657
  }
2650
2658
 
2651
- const cliOptions = {
2659
+ const cliOptions = withConfigCache({
2652
2660
  ...options,
2653
2661
  ...parsed.options,
2654
2662
  extraArgs: parsed.passthroughArgs,
2655
2663
  preserveNativeDefaults: Boolean(parsed.options.noConfig)
2656
- };
2664
+ });
2657
2665
  const targets = parsed.targets.length > 0 ? parsed.targets : defaultLintTargets(cliOptions);
2658
2666
  const report = lintFiles(targets, cliOptions);
2659
2667
  report.diagnostics = normalizeReportDiagnostics(report.diagnostics, cliOptions);
@@ -2662,7 +2670,7 @@ export function runCli(args = [], options = {}) {
2662
2670
  if (cliOptions.format === "json" || parsed.passthroughArgs.includes("--json")) {
2663
2671
  return completedRunResult(status, `${JSON.stringify(publicCliReport(report))}\n`, report.stderr ?? "", options);
2664
2672
  }
2665
- return completedRunResult(status, "", `${report.stderr ?? ""}${formatNativeTextReport(report)}`, options);
2673
+ return completedRunResult(status, "", `${report.stderr ?? ""}${formatNativeTextReport(report, cliOptions)}`, options);
2666
2674
  }
2667
2675
 
2668
2676
  function publicCliReport(report) {
@@ -2674,18 +2682,82 @@ function publicCliReport(report) {
2674
2682
  };
2675
2683
  }
2676
2684
 
2677
- function formatNativeTextReport(report) {
2685
+ function formatNativeTextReport(report, options = {}) {
2686
+ const diagnostics = report.diagnostics ?? [];
2687
+ const files = report.files ?? 0;
2688
+ const color = shouldUseColor(options);
2689
+
2690
+ if (diagnostics.length === 0) {
2691
+ return `${paint(color, "\u001b[32m", "✓")} ${pluralize(files, "file")} checked, no problems found\n`;
2692
+ }
2693
+
2678
2694
  const lines = [];
2679
- for (const diagnostic of report.diagnostics ?? []) {
2680
- const rule = diagnostic.ruleId ? ` [${diagnostic.ruleId}]` : "";
2695
+ const locationWidth = diagnostics.reduce(
2696
+ (width, diagnostic) => Math.max(width, diagnosticLocation(diagnostic).length),
2697
+ 0
2698
+ );
2699
+ let currentFile;
2700
+
2701
+ for (const diagnostic of diagnostics) {
2702
+ const filePath = diagnostic.filePath ?? "<unknown>";
2703
+ if (filePath !== currentFile) {
2704
+ if (lines.length > 0) lines.push("");
2705
+ lines.push(paint(color, "\u001b[1m", filePath));
2706
+ currentFile = filePath;
2707
+ }
2708
+
2709
+ const location = diagnosticLocation(diagnostic).padStart(locationWidth);
2710
+ const severity = diagnostic.severity === "warning" || diagnostic.severity === 1 ? "warning" : "error";
2711
+ const severityColor = severity === "error" ? "\u001b[31m" : "\u001b[33m";
2712
+ const message = String(diagnostic.message ?? "").replace(/\s*[\r\n]+\s*/g, " ");
2713
+ const rule = diagnostic.ruleId ? ` ${paint(color, "\u001b[2m", diagnostic.ruleId)}` : "";
2681
2714
  lines.push(
2682
- `${diagnostic.filePath}:${diagnostic.line ?? 0}:${diagnostic.column ?? 0}: ${diagnostic.severity ?? "error"}: ${diagnostic.message}${rule}`
2715
+ ` ${paint(color, "\u001b[2m", location)} ${paint(color, severityColor, severity.padEnd(7))} ${message}${rule}`
2683
2716
  );
2684
2717
  }
2685
- lines.push(`${report.files ?? 0} file(s) checked, ${(report.diagnostics ?? []).length} diagnostic(s)`);
2718
+
2719
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === 2).length;
2720
+ const warnings = diagnostics.length - errors;
2721
+ const markerColor = errors > 0 ? "\u001b[31m" : "\u001b[33m";
2722
+ lines.push("");
2723
+ lines.push(
2724
+ `${paint(color, markerColor, "✖")} ${pluralize(diagnostics.length, "problem")} (${pluralize(errors, "error")}, ${pluralize(warnings, "warning")})`
2725
+ );
2726
+
2727
+ const fixable = diagnostics.filter(diagnosticIsFixable).length;
2728
+ const fixRequested = options.fix || options.extraArgs?.includes("--fix") || options.extraArgs?.includes("--fix-dry-run");
2729
+ if (fixable > 0 && !fixRequested) {
2730
+ lines.push(` ${pluralize(fixable, "problem")} potentially fixable with the \`--fix\` option.`);
2731
+ }
2732
+ lines.push(` ${pluralize(files, "file")} checked`);
2686
2733
  return `${lines.join("\n")}\n`;
2687
2734
  }
2688
2735
 
2736
+ function diagnosticLocation(diagnostic) {
2737
+ return `${diagnostic.line ?? 0}:${diagnostic.column ?? 0}`;
2738
+ }
2739
+
2740
+ function diagnosticIsFixable(diagnostic) {
2741
+ return Boolean(diagnostic.fix) || (Array.isArray(diagnostic.fixes) && diagnostic.fixes.length > 0);
2742
+ }
2743
+
2744
+ function pluralize(count, noun) {
2745
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
2746
+ }
2747
+
2748
+ function paint(enabled, code, text) {
2749
+ return enabled ? `${code}${text}\u001b[0m` : text;
2750
+ }
2751
+
2752
+ function shouldUseColor(options = {}) {
2753
+ if (options.color === true) return true;
2754
+ if (options.color === false) return false;
2755
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2756
+ if (env.NO_COLOR !== undefined || env.NODE_DISABLE_COLORS !== undefined) return false;
2757
+ if ((env.FORCE_COLOR && env.FORCE_COLOR !== "0") || (env.CLICOLOR_FORCE && env.CLICOLOR_FORCE !== "0")) return true;
2758
+ return Boolean(process.stderr.isTTY);
2759
+ }
2760
+
2689
2761
  export function runFishlint(args = [], options = {}) {
2690
2762
  const cliArgs = normalizeStringArray(args, "args");
2691
2763
  const env = options.env ? { ...process.env, ...options.env } : { ...process.env };
@@ -2899,6 +2971,7 @@ function booleanFlagValue(arg) {
2899
2971
  }
2900
2972
 
2901
2973
  export function lintFiles(paths, options = {}) {
2974
+ options = withConfigCache(options);
2902
2975
  const targets = lintTargetsForInput(paths, options);
2903
2976
  const ignoredDiagnostics = ignoredLintPathDiagnostics(targets, options);
2904
2977
  const lintPaths = filteredLintPaths(targets, options);
@@ -3077,6 +3150,7 @@ function mergeNativeReports(reports) {
3077
3150
  }
3078
3151
 
3079
3152
  export function lintText(code, options = {}) {
3153
+ options = withConfigCache(options);
3080
3154
  if (typeof code !== "string") {
3081
3155
  throw new TypeError("code must be a string");
3082
3156
  }
@@ -3143,7 +3217,7 @@ function buildNativeLintArgs(paths, options) {
3143
3217
  if (!options.rules && !options.forceMaterializedConfig && (options.config || options.baseConfig || options.overrideConfig)) {
3144
3218
  const configForRuleSelection = options.noConfig
3145
3219
  ? undefined
3146
- : options.config ? readConfig(options.config, options.cwd) : undefined;
3220
+ : options.config ? readConfig(options.config, options.cwd, options.configCache) : undefined;
3147
3221
  const enabledRules = enabledRuleNamesFromConfigs(
3148
3222
  configForRuleSelection,
3149
3223
  options.baseConfig,
@@ -3189,6 +3263,12 @@ function mergeLintOptions(base, override) {
3189
3263
  };
3190
3264
  }
3191
3265
 
3266
+ function withConfigCache(options) {
3267
+ // Executable configs are otherwise re-evaluated at every per-file lookup.
3268
+ // Keep the cache on the options object so one public invocation shares it.
3269
+ return options.configCache ? options : { ...options, configCache: new Map() };
3270
+ }
3271
+
3192
3272
  function eslintConstructorOptions(options) {
3193
3273
  const mapped = {
3194
3274
  cwd: options.cwd,
@@ -3361,7 +3441,7 @@ function rulesFromFileConfig(options, filePath) {
3361
3441
  return {};
3362
3442
  }
3363
3443
 
3364
- const config = readConfig(configPath, options.cwd);
3444
+ const config = readConfig(configPath, options.cwd, options.configCache);
3365
3445
  return rulesFromConfig(config, filePath, dirname(configPath));
3366
3446
  }
3367
3447
 
@@ -3375,7 +3455,7 @@ function configDataFromFileConfig(options, filePath) {
3375
3455
  return {};
3376
3456
  }
3377
3457
 
3378
- const config = readConfig(configPath, options.cwd);
3458
+ const config = readConfig(configPath, options.cwd, options.configCache);
3379
3459
  return configDataFromConfig(config, filePath, dirname(configPath));
3380
3460
  }
3381
3461
 
@@ -3396,7 +3476,7 @@ function defaultLintTargets(options = {}) {
3396
3476
  return ["."];
3397
3477
  }
3398
3478
 
3399
- const targets = filePatternsFromConfig(readConfig(configPath, options.cwd));
3479
+ const targets = filePatternsFromConfig(readConfig(configPath, options.cwd, options.configCache));
3400
3480
  return targets.length > 0
3401
3481
  ? targets.map((target) => resolveConfigPattern(target, dirname(configPath)))
3402
3482
  : ["."];
@@ -3488,7 +3568,9 @@ function configPathFromDirectory(directory) {
3488
3568
  function withTemporaryConfig(options, callback) {
3489
3569
  const fileConfigPath = !options.noConfig ? configPathForOptions(options) : undefined;
3490
3570
  const shouldMaterializeFileConfig = Boolean(fileConfigPath);
3491
- const fileConfig = shouldMaterializeFileConfig ? readConfig(fileConfigPath, options.cwd) : undefined;
3571
+ const fileConfig = shouldMaterializeFileConfig
3572
+ ? readConfig(fileConfigPath, options.cwd, options.configCache)
3573
+ : undefined;
3492
3574
  const configs = [options.baseConfig, fileConfig, options.overrideConfig];
3493
3575
  const rules = shouldMaterializeFileConfig
3494
3576
  ? materializedRulesFromConfigs(...configs)
@@ -4015,7 +4097,7 @@ function ignorePatternsFromFileConfig(options, filePath) {
4015
4097
  }
4016
4098
 
4017
4099
  const cwd = options.cwd ?? process.cwd();
4018
- return ignorePatternsFromConfig(readConfig(configPath, options.cwd)).map((pattern) =>
4100
+ return ignorePatternsFromConfig(readConfig(configPath, options.cwd, options.configCache)).map((pattern) =>
4019
4101
  rebaseConfigPattern(pattern, dirname(configPath), cwd)
4020
4102
  );
4021
4103
  }
@@ -38,17 +38,25 @@ function isFile(path) {
38
38
  }
39
39
  }
40
40
 
41
- function readConfig(path, cwd) {
41
+ function readConfig(path, cwd, cache) {
42
42
  const configPath = resolvePath(cwd ?? process.cwd(), path);
43
- if (isExecutableConfigPath(configPath)) {
44
- return readExecutableConfig(configPath, cwd);
43
+ if (cache?.has(configPath)) {
44
+ return cache.get(configPath);
45
45
  }
46
46
 
47
- try {
48
- return JSON.parse(readFileSync(configPath, "utf8"));
49
- } catch (error) {
50
- throw configReadError(configPath, error.message);
47
+ let config;
48
+ if (isExecutableConfigPath(configPath)) {
49
+ config = readExecutableConfig(configPath, cwd);
50
+ } else {
51
+ try {
52
+ config = JSON.parse(readFileSync(configPath, "utf8"));
53
+ } catch (error) {
54
+ throw configReadError(configPath, error.message);
55
+ }
51
56
  }
57
+
58
+ cache?.set(configPath, config);
59
+ return config;
52
60
  }
53
61
 
54
62
  function isExecutableConfigPath(path) {
@@ -39,17 +39,25 @@ function isFile(path) {
39
39
  }
40
40
  }
41
41
 
42
- export function readConfig(path, cwd) {
42
+ export function readConfig(path, cwd, cache) {
43
43
  const configPath = resolvePath(cwd ?? process.cwd(), path);
44
- if (isExecutableConfigPath(configPath)) {
45
- return readExecutableConfig(configPath, cwd);
44
+ if (cache?.has(configPath)) {
45
+ return cache.get(configPath);
46
46
  }
47
47
 
48
- try {
49
- return JSON.parse(readFileSync(configPath, "utf8"));
50
- } catch (error) {
51
- throw configReadError(configPath, error.message);
48
+ let config;
49
+ if (isExecutableConfigPath(configPath)) {
50
+ config = readExecutableConfig(configPath, cwd);
51
+ } else {
52
+ try {
53
+ config = JSON.parse(readFileSync(configPath, "utf8"));
54
+ } catch (error) {
55
+ throw configReadError(configPath, error.message);
56
+ }
52
57
  }
58
+
59
+ cache?.set(configPath, config);
60
+ return config;
53
61
  }
54
62
 
55
63
  export function isExecutableConfigPath(path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utoo/lint",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Experimental JavaScript and TypeScript linter powered by Yuku",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -73,11 +73,11 @@
73
73
  "use-at-your-own-risk.js"
74
74
  ],
75
75
  "optionalDependencies": {
76
- "@utoo/lint-darwin-arm64": "0.2.2",
77
- "@utoo/lint-darwin-x64": "0.2.2",
78
- "@utoo/lint-linux-arm64": "0.2.2",
79
- "@utoo/lint-linux-x64": "0.2.2",
80
- "@utoo/lint-win32-x64": "0.2.2"
76
+ "@utoo/lint-darwin-arm64": "0.2.4",
77
+ "@utoo/lint-darwin-x64": "0.2.4",
78
+ "@utoo/lint-linux-arm64": "0.2.4",
79
+ "@utoo/lint-linux-x64": "0.2.4",
80
+ "@utoo/lint-win32-x64": "0.2.4"
81
81
  },
82
82
  "dependencies": {
83
83
  "jiti": "2.7.0"