eslint-interactive 13.0.1 → 14.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.
Files changed (111) hide show
  1. package/README.md +19 -10
  2. package/dist/action/convert-error-to-warning-per-file.d.ts +2 -4
  3. package/dist/action/convert-error-to-warning-per-file.d.ts.map +1 -1
  4. package/dist/action/convert-error-to-warning-per-file.js +2 -2
  5. package/dist/action/convert-error-to-warning-per-file.js.map +1 -1
  6. package/dist/action/disable-per-file.d.ts +2 -4
  7. package/dist/action/disable-per-file.d.ts.map +1 -1
  8. package/dist/action/disable-per-file.js +2 -2
  9. package/dist/action/disable-per-file.js.map +1 -1
  10. package/dist/action/disable-per-line.d.ts +2 -4
  11. package/dist/action/disable-per-line.d.ts.map +1 -1
  12. package/dist/action/disable-per-line.js +2 -2
  13. package/dist/action/disable-per-line.js.map +1 -1
  14. package/dist/action/fix.d.ts +2 -4
  15. package/dist/action/fix.d.ts.map +1 -1
  16. package/dist/action/fix.js +2 -2
  17. package/dist/action/fix.js.map +1 -1
  18. package/dist/action/print-result-details.d.ts +2 -3
  19. package/dist/action/print-result-details.d.ts.map +1 -1
  20. package/dist/action/print-result-details.js +3 -2
  21. package/dist/action/print-result-details.js.map +1 -1
  22. package/dist/cli/log.d.ts +1 -0
  23. package/dist/cli/log.d.ts.map +1 -1
  24. package/dist/cli/log.js +25 -0
  25. package/dist/cli/log.js.map +1 -1
  26. package/dist/cli/parse-argv.d.ts.map +1 -1
  27. package/dist/cli/parse-argv.js +31 -10
  28. package/dist/cli/parse-argv.js.map +1 -1
  29. package/dist/cli/prompt.d.ts.map +1 -1
  30. package/dist/cli/prompt.js +53 -91
  31. package/dist/cli/prompt.js.map +1 -1
  32. package/dist/cli/run.d.ts +1 -3
  33. package/dist/cli/run.d.ts.map +1 -1
  34. package/dist/cli/run.js +3 -27
  35. package/dist/cli/run.js.map +1 -1
  36. package/dist/core.d.ts +2 -2
  37. package/dist/core.d.ts.map +1 -1
  38. package/dist/core.js +10 -7
  39. package/dist/core.js.map +1 -1
  40. package/dist/fix/disable-per-file.d.ts +1 -1
  41. package/dist/fix/disable-per-file.d.ts.map +1 -1
  42. package/dist/fix/disable-per-line.d.ts +1 -1
  43. package/dist/fix/disable-per-line.d.ts.map +1 -1
  44. package/dist/formatter/filter-rule-statistics.d.ts +9 -0
  45. package/dist/formatter/filter-rule-statistics.d.ts.map +1 -0
  46. package/dist/formatter/filter-rule-statistics.js +22 -0
  47. package/dist/formatter/filter-rule-statistics.js.map +1 -0
  48. package/dist/formatter/format-by-rules.d.ts +4 -3
  49. package/dist/formatter/format-by-rules.d.ts.map +1 -1
  50. package/dist/formatter/format-by-rules.js +7 -5
  51. package/dist/formatter/format-by-rules.js.map +1 -1
  52. package/dist/formatter/index.d.ts +3 -2
  53. package/dist/formatter/index.d.ts.map +1 -1
  54. package/dist/formatter/index.js +3 -2
  55. package/dist/formatter/index.js.map +1 -1
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/scene/check-results.d.ts.map +1 -1
  60. package/dist/scene/check-results.js +2 -5
  61. package/dist/scene/check-results.js.map +1 -1
  62. package/dist/scene/lint.d.ts +2 -3
  63. package/dist/scene/lint.d.ts.map +1 -1
  64. package/dist/scene/lint.js +13 -9
  65. package/dist/scene/lint.js.map +1 -1
  66. package/dist/scene/select-action.d.ts +2 -3
  67. package/dist/scene/select-action.d.ts.map +1 -1
  68. package/dist/scene/select-action.js.map +1 -1
  69. package/dist/scene/select-rule-ids.d.ts +2 -3
  70. package/dist/scene/select-rule-ids.d.ts.map +1 -1
  71. package/dist/scene/select-rule-ids.js.map +1 -1
  72. package/dist/type.d.ts +2 -0
  73. package/dist/type.d.ts.map +1 -1
  74. package/dist/util/terminal-link.d.ts +2 -0
  75. package/dist/util/terminal-link.d.ts.map +1 -0
  76. package/dist/util/terminal-link.js +16 -0
  77. package/dist/util/terminal-link.js.map +1 -0
  78. package/package.json +5 -6
  79. package/src/action/convert-error-to-warning-per-file.ts +4 -6
  80. package/src/action/disable-per-file.ts +4 -6
  81. package/src/action/disable-per-line.ts +4 -6
  82. package/src/action/fix.ts +4 -10
  83. package/src/action/print-result-details.ts +5 -9
  84. package/src/cli/log.ts +25 -0
  85. package/src/cli/parse-argv.ts +32 -11
  86. package/src/cli/prompt.ts +56 -92
  87. package/src/cli/run.ts +3 -29
  88. package/src/core.ts +13 -6
  89. package/src/fix/disable-per-file.ts +1 -1
  90. package/src/fix/disable-per-line.ts +1 -1
  91. package/src/formatter/filter-rule-statistics.ts +27 -0
  92. package/src/formatter/format-by-rules.ts +10 -7
  93. package/src/formatter/index.ts +4 -3
  94. package/src/index.ts +1 -1
  95. package/src/scene/check-results.ts +2 -5
  96. package/src/scene/lint.ts +14 -12
  97. package/src/scene/select-action.ts +2 -4
  98. package/src/scene/select-rule-ids.ts +2 -6
  99. package/src/type.ts +2 -0
  100. package/src/util/terminal-link.ts +16 -0
  101. package/dist/cli/spinner.d.ts +0 -4
  102. package/dist/cli/spinner.d.ts.map +0 -1
  103. package/dist/cli/spinner.js +0 -20
  104. package/dist/cli/spinner.js.map +0 -1
  105. package/dist/core-worker.d.ts +0 -21
  106. package/dist/core-worker.d.ts.map +0 -1
  107. package/dist/core-worker.js +0 -47
  108. package/dist/core-worker.js.map +0 -1
  109. package/src/cli/spinner.ts +0 -22
  110. package/src/core-worker.ts +0 -55
  111. package/src/typings/enquirer.d.ts +0 -7
package/src/cli/prompt.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  /* istanbul ignore file */
2
2
 
3
- import enquirer from 'enquirer';
3
+ import { isCancel, multiselect, select, text } from '@clack/prompts';
4
4
  import type { ESLint } from 'eslint';
5
5
  import { takeRuleStatistics } from '../formatter/index.js';
6
6
 
7
- const { prompt } = enquirer;
8
-
9
- // When combined with worker, for some reason the enquirer grabs the SIGINT and the process continues to survive.
10
- // Therefore, the process is explicitly terminated.
11
- // eslint-disable-next-line n/no-process-exit
12
- const onCancel = () => process.exit();
7
+ function exitIfCancel<T>(value: T | symbol): T {
8
+ if (isCancel(value)) {
9
+ // eslint-disable-next-line n/no-process-exit
10
+ process.exit();
11
+ }
12
+ return value;
13
+ }
13
14
 
14
15
  /**
15
16
  * The type that indicates what to do with the problems of selected rules.
@@ -45,21 +46,13 @@ export type DescriptionPosition = 'sameLine' | 'previousLine';
45
46
  * @returns The rule ids
46
47
  */
47
48
  export async function promptToInputRuleIds(ruleIdsInResults: string[]): Promise<string[]> {
48
- const { ruleIds } = await prompt<{ ruleIds: string[] }>([
49
- {
50
- name: 'ruleIds',
51
- type: 'multiselect',
49
+ return exitIfCancel(
50
+ await multiselect<string>({
52
51
  message: 'Which rules would you like to apply action?',
53
- // @ts-expect-error
54
- hint: 'Select all you want with <space> key.',
55
- choices: ruleIdsInResults,
56
- validate(value) {
57
- return value.length === 0 ? `Select at least one rule with <space> key.` : true;
58
- },
59
- onCancel,
60
- },
61
- ]);
62
- return ruleIds;
52
+ options: ruleIdsInResults.map((ruleId) => ({ value: ruleId })),
53
+ required: true,
54
+ }),
55
+ );
63
56
  }
64
57
 
65
58
  /**
@@ -81,29 +74,21 @@ export async function promptToInputAction(
81
74
  { isFixableCount: 0 },
82
75
  );
83
76
 
84
- const choices = [
85
- { name: 'printResultDetails', message: '🔎 Display details of lint results' },
86
- { name: 'applyAutoFixes', message: '🔧 Run `eslint --fix`', disabled: foldedStatistics.isFixableCount === 0 },
87
- { name: 'disablePerLine', message: '🔧 Disable per line' },
88
- { name: 'disablePerFile', message: '🔧 Disable per file' },
89
- { name: 'convertErrorToWarningPerFile', message: '🔧 Convert error to warning per file' },
90
- { name: 'relintAndReselectRules', message: '↩️ Go back (with re-lint)' },
91
- { name: 'reselectRules', message: '↩️ Go back' },
92
- ];
93
-
94
- const { action } = await prompt<{
95
- action: Action;
96
- }>([
97
- {
98
- name: 'action',
99
- type: 'select',
77
+ return exitIfCancel(
78
+ await select<Action>({
100
79
  message: 'Which action do you want to do?',
101
- choices,
102
- initial: choices.findIndex((choice) => choice.name === initialAction) ?? 0,
103
- onCancel,
104
- },
105
- ]);
106
- return action;
80
+ options: [
81
+ { value: 'printResultDetails', label: '🔎 Display details of lint results' },
82
+ { value: 'applyAutoFixes', label: '🔧 Run `eslint --fix`', disabled: foldedStatistics.isFixableCount === 0 },
83
+ { value: 'disablePerLine', label: '🔧 Disable per line' },
84
+ { value: 'disablePerFile', label: '🔧 Disable per file' },
85
+ { value: 'convertErrorToWarningPerFile', label: '🔧 Convert error to warning per file' },
86
+ { value: 'relintAndReselectRules', label: '↩️ Go back (with re-lint)' },
87
+ { value: 'reselectRules', label: '↩️ Go back' },
88
+ ],
89
+ initialValue: initialAction,
90
+ }),
91
+ );
107
92
  }
108
93
 
109
94
  /**
@@ -111,22 +96,16 @@ export async function promptToInputAction(
111
96
  * @returns How to display
112
97
  */
113
98
  export async function promptToInputDisplayMode(): Promise<DisplayMode> {
114
- const { displayMode } = await prompt<{
115
- displayMode: DisplayMode;
116
- }>([
117
- {
118
- name: 'displayMode',
119
- type: 'select',
99
+ return exitIfCancel(
100
+ await select<DisplayMode>({
120
101
  message: 'In what way are the details displayed?',
121
- choices: [
122
- { name: 'printInTerminal', message: '🖨 Print in terminal' },
123
- { name: 'printInTerminalWithPager', message: '↕️ Print in terminal with pager' },
124
- { name: 'writeToFile', message: '📝 Write to file' },
102
+ options: [
103
+ { value: 'printInTerminal', label: '🖨 Print in terminal' },
104
+ { value: 'printInTerminalWithPager', label: '↕️ Print in terminal with pager' },
105
+ { value: 'writeToFile', label: '📝 Write to file' },
125
106
  ],
126
- onCancel,
127
- },
128
- ]);
129
- return displayMode;
107
+ }),
108
+ );
130
109
  }
131
110
 
132
111
  /**
@@ -134,17 +113,12 @@ export async function promptToInputDisplayMode(): Promise<DisplayMode> {
134
113
  * @returns The description
135
114
  */
136
115
  export async function promptToInputDescription(): Promise<string | undefined> {
137
- const { description } = await prompt<{
138
- description: string;
139
- }>([
140
- {
141
- name: 'description',
142
- type: 'input',
116
+ const description = exitIfCancel(
117
+ await text({
143
118
  message: 'Leave a code comment with your reason for fixing (Optional)',
144
- onCancel,
145
- },
146
- ]);
147
- return description === '' ? undefined : description;
119
+ }),
120
+ );
121
+ return description.trim() === '' ? undefined : description.trim();
148
122
  }
149
123
 
150
124
  /**
@@ -152,21 +126,15 @@ export async function promptToInputDescription(): Promise<string | undefined> {
152
126
  * @returns The description position
153
127
  */
154
128
  export async function promptToInputDescriptionPosition(): Promise<DescriptionPosition> {
155
- const { descriptionPosition } = await prompt<{
156
- descriptionPosition: DescriptionPosition;
157
- }>([
158
- {
159
- name: 'descriptionPosition',
160
- type: 'select',
129
+ return exitIfCancel(
130
+ await select<DescriptionPosition>({
161
131
  message: 'Where would you like to position the code comment?',
162
- choices: [
163
- { name: 'sameLine', message: "Same Line - Place on the same line as the eslint's disable comment." },
164
- { name: 'previousLine', message: "Previous Line - Place on the line before the eslint's disable comment." },
132
+ options: [
133
+ { value: 'sameLine', label: "Same Line - Place on the same line as the eslint's disable comment." },
134
+ { value: 'previousLine', label: "Previous Line - Place on the line before the eslint's disable comment." },
165
135
  ],
166
- onCancel,
167
- },
168
- ]);
169
- return descriptionPosition;
136
+ }),
137
+ );
170
138
  }
171
139
 
172
140
  /**
@@ -174,18 +142,14 @@ export async function promptToInputDescriptionPosition(): Promise<DescriptionPos
174
142
  * @returns What to do next.
175
143
  */
176
144
  export async function promptToInputWhatToDoNext(): Promise<NextStep> {
177
- const { nextStep } = await prompt<{ nextStep: NextStep }>([
178
- {
179
- name: 'nextStep',
180
- type: 'select',
145
+ return exitIfCancel(
146
+ await select<NextStep>({
181
147
  message: "What's the next step?",
182
- choices: [
183
- { name: 'fixOtherRules', message: '🔧 Fix other rules' },
184
- { name: 'undoTheFix', message: '↩️ Undo the fix' },
185
- { name: 'exit', message: '💚 Exit' },
148
+ options: [
149
+ { value: 'fixOtherRules', label: '🔧 Fix other rules' },
150
+ { value: 'undoTheFix', label: '↩️ Undo the fix' },
151
+ { value: 'exit', label: '💚 Exit' },
186
152
  ],
187
- onCancel,
188
- },
189
- ]);
190
- return nextStep;
153
+ }),
154
+ );
191
155
  }
package/src/cli/run.ts CHANGED
@@ -1,11 +1,5 @@
1
- import { dirname, join } from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import { Worker } from 'node:worker_threads';
4
- import { wrap } from 'comlink';
5
- import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
6
- import terminalLink from 'terminal-link';
7
1
  import { parseArgv } from '../cli/parse-argv.js';
8
- import type { SerializableCore } from '../core-worker.js';
2
+ import { Core } from '../core.js';
9
3
  import type { NextScene } from '../scene/index.js';
10
4
  import { checkResults, lint, selectAction, selectRuleIds } from '../scene/index.js';
11
5
 
@@ -13,29 +7,10 @@ export type Options = {
13
7
  argv: string[];
14
8
  };
15
9
 
16
- /**
17
- * Run eslint-interactive.
18
- */
10
+ /** Run eslint-interactive. */
19
11
  export async function run(options: Options) {
20
12
  const config = parseArgv(options.argv);
21
-
22
- // Directly executing the Core API will hog the main thread and halt the spinner.
23
- // So we wrap it with comlink and run it on the Worker.
24
- const worker = new Worker(join(dirname(fileURLToPath(import.meta.url)), '..', 'core-worker.js'), {
25
- env: {
26
- // In worker threads, stdin is recognized as noTTY. Therefore, `util.styleText` and `terminalLink` disable colors and links.
27
- // To work around this, we use environment variables to force colors and links to be enabled.
28
- // ref: https://github.com/nodejs/node/issues/26946
29
- FORCE_COLOR: process.stdin.isTTY ? '1' : '0',
30
- FORCE_HYPERLINK: terminalLink.isSupported ? '1' : '0',
31
- ...process.env,
32
- },
33
- // NOTE: Pass CLI options (--unhandled-rejections=strict, etc.) to the worker
34
- execArgv: process.execArgv,
35
- });
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- const ProxiedCore = wrap<typeof SerializableCore>((nodeEndpoint as any)(worker));
38
- const core = await new ProxiedCore(config);
13
+ const core = new Core(config);
39
14
 
40
15
  let nextScene: NextScene = { name: 'lint' };
41
16
  while (nextScene.name !== 'exit') {
@@ -53,5 +28,4 @@ export async function run(options: Options) {
53
28
  nextScene = await checkResults(nextScene.args);
54
29
  }
55
30
  }
56
- await worker.terminate();
57
31
  }
package/src/core.ts CHANGED
@@ -12,9 +12,9 @@ import {
12
12
  createFixToMakeFixableAndFix,
13
13
  verifyAndFix,
14
14
  } from './fix/index.js';
15
- import { format, sortRuleStatistics, takeRuleStatistics } from './formatter/index.js';
15
+ import { filterRuleStatistics, format, sortRuleStatistics, takeRuleStatistics } from './formatter/index.js';
16
16
  import { plugin } from './plugin.js';
17
- import type { Config, SortField, SortOrder } from './type.js';
17
+ import type { Config, FilterCriterion, SortField, SortOrder } from './type.js';
18
18
  import { filterResultsByRuleId } from './util/eslint.js';
19
19
 
20
20
  /**
@@ -42,6 +42,7 @@ export class Core {
42
42
  readonly #formatterName: string | undefined;
43
43
  readonly #sort: SortField | undefined;
44
44
  readonly #sortOrder: SortOrder | undefined;
45
+ readonly #filters: FilterCriterion[] | undefined;
45
46
  readonly #eslint: ESLint;
46
47
 
47
48
  constructor(config: Config) {
@@ -51,10 +52,11 @@ export class Core {
51
52
  this.#formatterName = config.formatterName;
52
53
  this.#sort = config.sort;
53
54
  this.#sortOrder = config.sortOrder;
55
+ this.#filters = config.filters;
54
56
 
55
57
  // NOTE: Passing an option that does not exist to `new ESLint(...)` will throw an error.
56
58
  // Therefore, only options supported by ESLint are extracted into the `eslintOptions` variable.
57
- const { formatterName, patterns, quiet, sort, sortOrder, ...eslintOptions } = config;
59
+ const { formatterName, patterns, quiet, sort, sortOrder, filters, ...eslintOptions } = config;
58
60
  const overrideConfigs =
59
61
  Array.isArray(eslintOptions.overrideConfig) ? eslintOptions.overrideConfig
60
62
  : eslintOptions.overrideConfig ? [eslintOptions.overrideConfig]
@@ -89,15 +91,20 @@ export class Core {
89
91
  */
90
92
  formatResultSummary(results: ESLint.LintResult[]): string {
91
93
  const rulesMeta = this.#eslint.getRulesMetaForResults(results);
92
- return format(results, { rulesMeta, cwd: this.#cwd }, { sort: this.#sort, sortOrder: this.#sortOrder });
94
+ return format(
95
+ results,
96
+ { rulesMeta, cwd: this.#cwd },
97
+ { sort: this.#sort, sortOrder: this.#sortOrder, filters: this.#filters },
98
+ );
93
99
  }
94
100
 
95
101
  /**
96
- * Returns ruleIds from lint results, sorted according to the configured sort options.
102
+ * Returns ruleIds from lint results, filtered and sorted according to the configured options.
97
103
  * @param results The lint results of the project
98
104
  */
99
- getSortedRuleIdsInResults(results: ESLint.LintResult[]): string[] {
105
+ getFilteredAndSortedRuleIds(results: ESLint.LintResult[]): string[] {
100
106
  let ruleStatistics = takeRuleStatistics(results);
107
+ ruleStatistics = filterRuleStatistics(ruleStatistics, this.#filters);
101
108
  if (this.#sort) {
102
109
  ruleStatistics = sortRuleStatistics(ruleStatistics, this.#sort, this.#sortOrder);
103
110
  }
@@ -1,5 +1,5 @@
1
1
  import type { Rule, SourceCode } from 'eslint';
2
- import type { DescriptionPosition } from 'src/cli/prompt.js';
2
+ import type { DescriptionPosition } from '../cli/prompt.js';
3
3
  import { mergeFixes } from '../eslint/report-translator.js';
4
4
  import { unique } from '../util/array.js';
5
5
  import type { DisableComment } from '../util/eslint.js';
@@ -1,5 +1,5 @@
1
1
  import type { Linter, Rule, SourceCode } from 'eslint';
2
- import type { DescriptionPosition } from 'src/cli/prompt.js';
2
+ import type { DescriptionPosition } from '../cli/prompt.js';
3
3
  import { mergeFixes } from '../eslint/report-translator.js';
4
4
  import { groupBy, unique } from '../util/array.js';
5
5
  import type { DisableComment } from '../util/eslint.js';
@@ -0,0 +1,27 @@
1
+ import type { FilterCriterion } from '../type.js';
2
+ import { unreachable } from '../util/type-check.js';
3
+ import type { RuleStatistic } from './take-rule-statistics.js';
4
+
5
+ function matchesCriterion(statistic: RuleStatistic, criterion: FilterCriterion): boolean {
6
+ switch (criterion) {
7
+ case 'fixable':
8
+ return statistic.isFixableCount > 0;
9
+ case 'has-suggestions':
10
+ return statistic.hasSuggestionsCount > 0;
11
+ default:
12
+ return unreachable(`Invalid filter criterion: ${criterion as string}`);
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Filter rule statistics by the given criteria.
18
+ * Multiple criteria are OR-ed together; a statistic is kept if it matches any of them.
19
+ * When `criteria` is undefined or empty, the input is returned unchanged.
20
+ */
21
+ export function filterRuleStatistics(
22
+ statistics: RuleStatistic[],
23
+ criteria: readonly FilterCriterion[] | undefined,
24
+ ): RuleStatistic[] {
25
+ if (!criteria || criteria.length === 0) return statistics;
26
+ return statistics.filter((statistic) => criteria.some((criterion) => matchesCriterion(statistic, criterion)));
27
+ }
@@ -1,9 +1,10 @@
1
1
  // eslint-disable-next-line n/no-unsupported-features/node-builtins
2
2
  import { styleText } from 'node:util';
3
3
  import type { ESLint } from 'eslint';
4
- import terminalLink from 'terminal-link';
5
- import type { SortField, SortOrder } from '../type.js';
4
+ import type { FilterCriterion, SortField, SortOrder } from '../type.js';
5
+ import { terminalLink } from '../util/terminal-link.js';
6
6
  import { ERROR_COLOR } from './colors.js';
7
+ import { filterRuleStatistics } from './filter-rule-statistics.js';
7
8
  import { formatTable } from './format-table.js';
8
9
  import { sortRuleStatistics } from './sort-rule-statistics.js';
9
10
  import { takeRuleStatistics } from './take-rule-statistics.js';
@@ -18,9 +19,10 @@ type Row = [
18
19
  hasSuggestionsCount: string,
19
20
  ];
20
21
 
21
- export type FormatByRulesSortOptions = {
22
+ export type FormatByRulesOptions = {
22
23
  sort?: SortField | undefined;
23
24
  sortOrder?: SortOrder | undefined;
25
+ filters?: FilterCriterion[] | undefined;
24
26
  };
25
27
 
26
28
  function numCell(num: number): string {
@@ -30,11 +32,12 @@ function numCell(num: number): string {
30
32
  export function formatByRules(
31
33
  results: ESLint.LintResult[],
32
34
  data?: ESLint.LintResultData,
33
- sortOptions?: FormatByRulesSortOptions,
35
+ options?: FormatByRulesOptions,
34
36
  ): string {
35
37
  let ruleStatistics = takeRuleStatistics(results);
36
- if (sortOptions?.sort) {
37
- ruleStatistics = sortRuleStatistics(ruleStatistics, sortOptions.sort, sortOptions.sortOrder);
38
+ ruleStatistics = filterRuleStatistics(ruleStatistics, options?.filters);
39
+ if (options?.sort) {
40
+ ruleStatistics = sortRuleStatistics(ruleStatistics, options.sort, options.sortOrder);
38
41
  }
39
42
 
40
43
  const rows: Row[] = [];
@@ -42,7 +45,7 @@ export function formatByRules(
42
45
  const { ruleId, errorCount, warningCount, isFixableCount, hasSuggestionsCount } = ruleStatistic;
43
46
  const ruleMetaData = data?.rulesMeta[ruleId];
44
47
  rows.push([
45
- ruleMetaData?.docs?.url ? terminalLink(ruleId, ruleMetaData?.docs.url, { fallback: false }) : ruleId,
48
+ ruleMetaData?.docs?.url ? terminalLink(ruleId, ruleMetaData?.docs.url) : ruleId,
46
49
  numCell(errorCount),
47
50
  numCell(warningCount),
48
51
  numCell(isFixableCount),
@@ -1,14 +1,15 @@
1
1
  import type { ESLint } from 'eslint';
2
2
  import { formatByFiles } from './format-by-files.js';
3
- import { formatByRules, type FormatByRulesSortOptions } from './format-by-rules.js';
3
+ import { formatByRules, type FormatByRulesOptions } from './format-by-rules.js';
4
4
 
5
5
  export { takeRuleStatistics, type RuleStatistic } from './take-rule-statistics.js';
6
6
  export { sortRuleStatistics } from './sort-rule-statistics.js';
7
+ export { filterRuleStatistics } from './filter-rule-statistics.js';
7
8
 
8
9
  export function format(
9
10
  results: ESLint.LintResult[],
10
11
  data?: ESLint.LintResultData,
11
- sortOptions?: FormatByRulesSortOptions,
12
+ options?: FormatByRulesOptions,
12
13
  ): string {
13
- return `${formatByFiles(results)}\n${formatByRules(results, data, sortOptions)}`;
14
+ return `${formatByFiles(results)}\n${formatByRules(results, data, options)}`;
14
15
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { run, type Options } from './cli/run.js';
2
2
  export { Core } from './core.js';
3
- export { type SortField, type SortOrder, type Config } from './type.js';
3
+ export { type SortField, type SortOrder, type FilterCriterion, type Config } from './type.js';
4
4
  export { takeRuleStatistics, type RuleStatistic, sortRuleStatistics } from './formatter/index.js';
5
5
  export { type FixableMaker, type SuggestionFilter, type FixContext } from './fix/index.js';
@@ -1,7 +1,7 @@
1
1
  import type { ESLint } from 'eslint';
2
+ import { withProgress } from '../cli/log.js';
2
3
  import type { Action } from '../cli/prompt.js';
3
4
  import { promptToInputWhatToDoNext } from '../cli/prompt.js';
4
- import { undoingSpinner } from '../cli/spinner.js';
5
5
  import type { Undo } from '../core.js';
6
6
  import type { NextScene } from './index.js';
7
7
 
@@ -31,14 +31,11 @@ export async function checkResults({
31
31
  const nextStep = await promptToInputWhatToDoNext();
32
32
  if (nextStep === 'exit') return { name: 'exit' };
33
33
  if (nextStep === 'undoTheFix') {
34
- await undoingSpinner(async () => undo());
34
+ await withProgress('Undoing', async () => undo());
35
35
  return {
36
36
  name: 'selectAction',
37
37
  args: { results, ruleIdsInResults, selectedRuleIds, initialAction: selectedAction },
38
38
  };
39
39
  }
40
- console.log();
41
- console.log('─'.repeat(process.stdout.columns));
42
- console.log();
43
40
  return { name: 'lint' };
44
41
  }
package/src/scene/lint.ts CHANGED
@@ -1,15 +1,13 @@
1
- import type { Remote } from 'comlink';
2
- import { error } from '../cli/log.js';
3
- import { lintingSpinner } from '../cli/spinner.js';
4
- import type { SerializableCore } from '../core-worker.js';
1
+ import { log } from '@clack/prompts';
2
+ import { error, withProgress } from '../cli/log.js';
3
+ import type { Core } from '../core.js';
5
4
  import type { NextScene } from './index.js';
6
5
 
7
6
  /**
8
7
  * Run the scene to lint.
9
8
  */
10
- export async function lint(core: Remote<SerializableCore>): Promise<NextScene> {
11
- const results = await lintingSpinner(async () => core.lint());
12
- console.log();
9
+ export async function lint(core: Core): Promise<NextScene> {
10
+ const results = await withProgress('Linting', async () => core.lint());
13
11
 
14
12
  // Check for ESLint core problems (ruleId === null) first.
15
13
  // These represent config errors, syntax errors, etc. that eslint-interactive cannot fix.
@@ -21,19 +19,23 @@ export async function lint(core: Remote<SerializableCore>): Promise<NextScene> {
21
19
  'Check the details of the problem and fix it. ' +
22
20
  'This is usually caused by the invalid eslint config or the invalid syntax of the linted code.',
23
21
  );
24
- console.log(await core.formatResultDetails(results, [null]));
22
+ log.message(await core.formatResultDetails(results, [null]), {});
25
23
  // eslint-disable-next-line n/no-process-exit
26
24
  process.exit(1);
27
25
  }
28
26
 
29
- const ruleIdsInResults = await core.getSortedRuleIdsInResults(results);
27
+ const ruleIdsInResults = core.getFilteredAndSortedRuleIds(results);
30
28
 
31
29
  if (ruleIdsInResults.length === 0) {
32
- console.log('💚 No error found.');
30
+ const hasAnyMessage = results.some((result) => result.messages.length > 0);
31
+ if (hasAnyMessage) {
32
+ log.message('💚 No rules match the given --filter.');
33
+ } else {
34
+ log.message('💚 No rules with problems.');
35
+ }
33
36
  return { name: 'exit' };
34
37
  }
35
- console.log(await core.formatResultSummary(results));
38
+ log.message(core.formatResultSummary(results));
36
39
 
37
- console.log();
38
40
  return { name: 'selectRuleIds', args: { results, ruleIdsInResults } };
39
41
  }
@@ -1,4 +1,3 @@
1
- import type { Remote } from 'comlink';
2
1
  import type { ESLint } from 'eslint';
3
2
  import {
4
3
  doConvertErrorToWarningPerFileAction,
@@ -9,8 +8,7 @@ import {
9
8
  } from '../action/index.js';
10
9
  import type { Action } from '../cli/prompt.js';
11
10
  import { promptToInputAction } from '../cli/prompt.js';
12
- import type { Undo } from '../core.js';
13
- import type { SerializableCore } from '../core-worker.js';
11
+ import type { Core, Undo } from '../core.js';
14
12
  import { unreachable } from '../util/type-check.js';
15
13
  import type { NextScene } from './index.js';
16
14
 
@@ -29,7 +27,7 @@ export type SelectActionArgs = {
29
27
  * Run the scene where a user select the action to be performed for the problems of selected rules.
30
28
  */
31
29
  export async function selectAction(
32
- core: Remote<SerializableCore>,
30
+ core: Core,
33
31
  { results, ruleIdsInResults, selectedRuleIds, initialAction }: SelectActionArgs,
34
32
  ): Promise<NextScene> {
35
33
  const selectedAction = await promptToInputAction(results, selectedRuleIds, initialAction);
@@ -1,7 +1,6 @@
1
- import type { Remote } from 'comlink';
2
1
  import type { ESLint } from 'eslint';
3
2
  import { promptToInputRuleIds } from '../cli/prompt.js';
4
- import type { SerializableCore } from '../core-worker.js';
3
+ import type { Core } from '../core.js';
5
4
  import type { NextScene } from './index.js';
6
5
  import { selectAction } from './select-action.js';
7
6
 
@@ -15,10 +14,7 @@ export type SelectRuleIdsArgs = {
15
14
  /**
16
15
  * Run the scene where a user select rule ids.
17
16
  */
18
- export async function selectRuleIds(
19
- core: Remote<SerializableCore>,
20
- { results, ruleIdsInResults }: SelectRuleIdsArgs,
21
- ): Promise<NextScene> {
17
+ export async function selectRuleIds(core: Core, { results, ruleIdsInResults }: SelectRuleIdsArgs): Promise<NextScene> {
22
18
  const selectedRuleIds = await promptToInputRuleIds(ruleIdsInResults);
23
19
  return selectAction(core, { results, ruleIdsInResults, selectedRuleIds });
24
20
  }
package/src/type.ts CHANGED
@@ -2,6 +2,7 @@ import type { ESLint } from 'eslint';
2
2
 
3
3
  export type SortField = 'rule' | 'error' | 'warning' | 'fixable' | 'suggestions';
4
4
  export type SortOrder = 'asc' | 'desc';
5
+ export type FilterCriterion = 'fixable' | 'has-suggestions';
5
6
 
6
7
  /** The config of eslint-interactive */
7
8
  export type Config = ESLint.Options & {
@@ -10,4 +11,5 @@ export type Config = ESLint.Options & {
10
11
  quiet?: boolean | undefined;
11
12
  sort?: SortField | undefined;
12
13
  sortOrder?: SortOrder | undefined;
14
+ filters?: FilterCriterion[] | undefined;
13
15
  };
@@ -0,0 +1,16 @@
1
+ const OSC = '\x1B]';
2
+ const BEL = '\x07';
3
+
4
+ function buildOsc8Link(text: string, url: string): string {
5
+ return `${OSC}8;;${url}${BEL}${text}${OSC}8;;${BEL}`;
6
+ }
7
+
8
+ function supportsHyperlinks(): boolean {
9
+ if (process.env['FORCE_HYPERLINK'] === '1') return true;
10
+ if (process.env['FORCE_HYPERLINK'] === '0') return false;
11
+ return process.stdout.isTTY;
12
+ }
13
+
14
+ export function terminalLink(text: string, url: string): string {
15
+ return supportsHyperlinks() ? buildOsc8Link(text, url) : text;
16
+ }
@@ -1,4 +0,0 @@
1
- export declare function lintingSpinner<T>(cb: () => Promise<T>): Promise<T>;
2
- export declare function fixingSpinner<T>(cb: () => Promise<T>): Promise<T>;
3
- export declare function undoingSpinner<T>(cb: () => Promise<T>): Promise<T>;
4
- //# sourceMappingURL=spinner.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../src/cli/spinner.ts"],"names":[],"mappings":"AAEA,wBAAsB,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAKxE;AAED,wBAAsB,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAKvE;AAED,wBAAsB,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAKxE"}
@@ -1,20 +0,0 @@
1
- import { createSpinner } from 'nanospinner';
2
- export async function lintingSpinner(cb) {
3
- const spinner = createSpinner('Linting...').start();
4
- const result = await cb();
5
- spinner.success();
6
- return result;
7
- }
8
- export async function fixingSpinner(cb) {
9
- const spinner = createSpinner('Fixing...').start();
10
- const result = await cb();
11
- spinner.success();
12
- return result;
13
- }
14
- export async function undoingSpinner(cb) {
15
- const spinner = createSpinner('Undoing...').start();
16
- const result = await cb();
17
- spinner.success();
18
- return result;
19
- }
20
- //# sourceMappingURL=spinner.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"spinner.js","sourceRoot":"","sources":["../../src/cli/spinner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,cAAc,CAAI,EAAoB;IAC1D,MAAM,OAAO,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC,KAAK,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;IAC1B,OAAO,CAAC,OAAO,EAAE,CAAC;IAClB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAI,EAAoB;IACzD,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,CAAC;IACnD,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;IAC1B,OAAO,CAAC,OAAO,EAAE,CAAC;IAClB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAI,EAAoB;IAC1D,MAAM,OAAO,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC,KAAK,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;IAC1B,OAAO,CAAC,OAAO,EAAE,CAAC;IAClB,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,21 +0,0 @@
1
- import { Core } from './core.js';
2
- import type { Config } from './type.js';
3
- /**
4
- * This is a wrapper for using the Core API from comlink.
5
- *
6
- * The arguments of the methods wrapped in comlink must be serializable.
7
- * The methods in this class are serializable versions of the Core API methods.
8
- */
9
- export declare class SerializableCore {
10
- readonly core: Core;
11
- constructor(config: Config);
12
- lint(...args: Parameters<Core['lint']>): ReturnType<Core['lint']>;
13
- formatResultSummary(...args: Parameters<Core['formatResultSummary']>): ReturnType<Core['formatResultSummary']>;
14
- getSortedRuleIdsInResults(...args: Parameters<Core['getSortedRuleIdsInResults']>): ReturnType<Core['getSortedRuleIdsInResults']>;
15
- formatResultDetails(...args: Parameters<Core['formatResultDetails']>): ReturnType<Core['formatResultDetails']>;
16
- applyAutoFixes(...args: Parameters<Core['applyAutoFixes']>): ReturnType<Core['applyAutoFixes']>;
17
- disablePerLine(...args: Parameters<Core['disablePerLine']>): ReturnType<Core['disablePerLine']>;
18
- disablePerFile(...args: Parameters<Core['disablePerFile']>): ReturnType<Core['disablePerFile']>;
19
- convertErrorToWarningPerFile(...args: Parameters<Core['convertErrorToWarningPerFile']>): ReturnType<Core['convertErrorToWarningPerFile']>;
20
- }
21
- //# sourceMappingURL=core-worker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"core-worker.d.ts","sourceRoot":"","sources":["../src/core-worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAQxC;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;gBACR,MAAM,EAAE,MAAM;IAGpB,IAAI,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAGvE,mBAAmB,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAG9G,yBAAyB,CACvB,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,GACrD,UAAU,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAG1C,mBAAmB,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAG9G,cAAc,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAG/F,cAAc,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAG/F,cAAc,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAG/F,4BAA4B,CAChC,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,GACxD,UAAU,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;CAGpD"}