@yuu1111/quality-check 0.4.0 → 0.5.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.ja.md CHANGED
@@ -35,7 +35,7 @@ export default defineConfig({
35
35
  "tsdoc-check": true,
36
36
  },
37
37
  config: {
38
- "comment-check": { ignore: ["FFXIVReplayAnalyzer"] },
38
+ "comment-check": { ignore: ["another-project"] },
39
39
  "tsdoc-check": { error: ["missing-doc"] },
40
40
  },
41
41
  });
@@ -96,6 +96,10 @@ engineは `biome` → `typecheck` → `knip` → `comment-check` → `document-s
96
96
 
97
97
  終了codeは0が全engine成功、1が失敗したengineあり、2が設定またはengine起動の失敗
98
98
 
99
+ 色は標準出力が端末のときだけ付ける `NO_COLOR` で無効にし、`FORCE_COLOR` で強制できる `--json` の出力には付けない
100
+
101
+ 色を扱えるengineへは自身の出力の色も許可する Biomeは `--colors=force`、tscは `--pretty` を受け取る
102
+
99
103
  engineが受け取らない条件を書いた場合は渡さず、その旨をそのengineのsectionへ出す
100
104
 
101
105
  ```text
package/README.md CHANGED
@@ -38,7 +38,7 @@ export default defineConfig({
38
38
  "tsdoc-check": true,
39
39
  },
40
40
  config: {
41
- "comment-check": { ignore: ["FFXIVReplayAnalyzer"] },
41
+ "comment-check": { ignore: ["another-project"] },
42
42
  "tsdoc-check": { error: ["missing-doc"] },
43
43
  },
44
44
  });
@@ -104,6 +104,12 @@ express and for the case where an engine changes its arguments.
104
104
  Exit code 0 means every engine passed, 1 that at least one failed, and 2 that the
105
105
  configuration or an engine could not start.
106
106
 
107
+ Color is added only when stdout is a terminal. `NO_COLOR` turns it off and
108
+ `FORCE_COLOR` turns it on; the `--json` output stays plain.
109
+
110
+ The engines that can color their own output receive the same permission: Biome
111
+ gets `--colors=force` and tsc gets `--pretty`.
112
+
107
113
  A condition that an engine does not take is not passed on, and the section says
108
114
  so:
109
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuu1111/quality-check",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Integrated quality check runner",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { tmpdir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { createBaseline, readBaseline, writeBaseline } from "./baseline";
5
+ import { ansiPainter, colorEnabled, plainPainter } from "./color";
5
6
  import {
6
7
  DEFAULT_BASELINE_FILE,
7
8
  findConfigFile,
@@ -125,6 +126,8 @@ async function main(argv: string[]): Promise<number> {
125
126
  return 0;
126
127
  }
127
128
  const options = parseArguments(argv);
129
+ const color = colorEnabled(process.stdout, process.env);
130
+ const paint = color ? ansiPainter() : plainPainter;
128
131
  const cwd = process.cwd();
129
132
  const configPath = resolveConfigPath(options, cwd);
130
133
  if (configPath === null) {
@@ -138,6 +141,7 @@ async function main(argv: string[]): Promise<number> {
138
141
  options.update || baselinePath === null
139
142
  ? null
140
143
  : readBaseline(baselinePath),
144
+ color,
141
145
  config,
142
146
  cwd,
143
147
  overrides: { ignore: options.ignores, targets: options.targets },
@@ -153,7 +157,10 @@ async function main(argv: string[]): Promise<number> {
153
157
  );
154
158
  writeBaseline(baselinePath, baseline);
155
159
  console.log(
156
- `Recorded ${baseline.entries.length} entries in ${baselinePath}`,
160
+ paint(
161
+ `Recorded ${baseline.entries.length} entries in ${baselinePath}`,
162
+ "pass",
163
+ ),
157
164
  );
158
165
  return 0;
159
166
  }
@@ -161,9 +168,9 @@ async function main(argv: string[]): Promise<number> {
161
168
  console.log(JSON.stringify(toJsonReport(results), null, "\t"));
162
169
  } else {
163
170
  for (const result of results) {
164
- console.log(formatEngineSection(result));
171
+ console.log(formatEngineSection(result, paint));
165
172
  }
166
- console.log(formatSummary(results));
173
+ console.log(formatSummary(results, paint));
167
174
  }
168
175
  return results.some((result) => result.status !== "passed") ? 1 : 0;
169
176
  }
package/src/color.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 出力行へ割り当てる装飾の種類
3
+ */
4
+ export type Tone = "error" | "header" | "muted" | "pass" | "warn";
5
+
6
+ /**
7
+ * toneに応じて文字列を装飾する関数
8
+ */
9
+ export type Painter = (text: string, tone: Tone) => string;
10
+
11
+ const ANSI_CODES: Record<Tone, string> = {
12
+ error: "31",
13
+ header: "36",
14
+ muted: "2",
15
+ pass: "32",
16
+ warn: "33",
17
+ };
18
+
19
+ /**
20
+ * 装飾を付けずにそのまま返すpainter
21
+ */
22
+ export function plainPainter(text: string, _tone: Tone): string {
23
+ return text;
24
+ }
25
+
26
+ /**
27
+ * ANSI escapeでtoneを色へ割り当てるpainterを作る
28
+ */
29
+ export function ansiPainter(): Painter {
30
+ return (text, tone) => `\u001b[${ANSI_CODES[tone]}m${text}\u001b[0m`;
31
+ }
32
+
33
+ /**
34
+ * 出力先と環境変数から装飾の可否を決める
35
+ */
36
+ export function colorEnabled(
37
+ stream: { isTTY?: boolean | undefined },
38
+ env: Record<string, string | undefined>,
39
+ ): boolean {
40
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") {
41
+ return false;
42
+ }
43
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "") {
44
+ return env.FORCE_COLOR !== "0";
45
+ }
46
+ return stream.isTTY === true;
47
+ }
package/src/engines.ts CHANGED
@@ -52,6 +52,8 @@ export interface RunOverrides {
52
52
  */
53
53
  export interface EngineCommandContext {
54
54
  config: QualityConfig;
55
+ /** engine自身の出力へ色を付けるか */
56
+ color: boolean;
55
57
  /** 対応するengineへだけ足す上書き */
56
58
  overrides: RunOverrides;
57
59
  /** comment-checkのbaseline差分を無効化するために渡す未作成のpath */
@@ -143,6 +145,22 @@ export function resolveExecutable(
143
145
  return onPath ?? null;
144
146
  }
145
147
 
148
+ /**
149
+ * engine自身の出力へ色を付けるための引数を返す
150
+ */
151
+ function colorArguments(name: EngineName, color: boolean): string[] {
152
+ if (!color) {
153
+ return [];
154
+ }
155
+ if (name === "biome") {
156
+ return ["--colors=force"];
157
+ }
158
+ if (name === "typecheck") {
159
+ return ["--pretty"];
160
+ }
161
+ return [];
162
+ }
163
+
146
164
  /**
147
165
  * engineへ渡すコマンドを組み立てる
148
166
  */
@@ -167,10 +185,21 @@ export function buildEngineCommand(
167
185
  const rules = engineConfig(context.config, "tsdoc-check")?.error ?? [];
168
186
  const errorArguments = rules.flatMap((rule) => ["--error", rule]);
169
187
  if (name === "biome") {
170
- return [executable, "check", ...targets, ...extra];
188
+ return [
189
+ executable,
190
+ "check",
191
+ ...colorArguments(name, context.color),
192
+ ...targets,
193
+ ...extra,
194
+ ];
171
195
  }
172
196
  if (name === "typecheck") {
173
- return [executable, "--noEmit", ...extra];
197
+ return [
198
+ executable,
199
+ "--noEmit",
200
+ ...colorArguments(name, context.color),
201
+ ...extra,
202
+ ];
174
203
  }
175
204
  if (name === "knip") {
176
205
  return [executable, ...extra];
package/src/report.ts CHANGED
@@ -1,9 +1,14 @@
1
+ import { type Painter, plainPainter } from "./color";
1
2
  import { isFindingEngine } from "./engines";
2
3
  import type { NormalizedFinding } from "./findings";
3
4
  import type { EngineResult } from "./run";
4
5
 
5
- function describeFinding(finding: NormalizedFinding): string {
6
- return `${finding.file}:${finding.line}:${finding.column} ${finding.rule} ${finding.severity} ${finding.text}`;
6
+ function describeFinding(
7
+ finding: NormalizedFinding,
8
+ paint: Painter,
9
+ tone: "error" | "warn",
10
+ ): string {
11
+ return `${finding.file}:${finding.line}:${finding.column} ${finding.rule} ${paint(finding.severity, tone)} ${finding.text}`;
7
12
  }
8
13
 
9
14
  function describeCounts(result: EngineResult): string {
@@ -13,38 +18,48 @@ function describeCounts(result: EngineResult): string {
13
18
  return `${result.reported.length} new, ${result.resolved} resolved, ${result.warnings.length} warnings`;
14
19
  }
15
20
 
16
- function describeStatus(result: EngineResult): string {
21
+ function describeStatus(result: EngineResult, paint: Painter): string {
17
22
  if (result.status === "error") {
18
- return "error";
23
+ return paint("error", "error");
19
24
  }
20
- return `${result.status} (${describeCounts(result)})`;
25
+ const tone = result.status === "passed" ? "pass" : "error";
26
+ return `${paint(result.status, tone)} (${describeCounts(result)})`;
21
27
  }
22
28
 
23
29
  /**
24
30
  * engine1つ分の出力sectionを組み立てる
25
31
  */
26
- export function formatEngineSection(result: EngineResult): string {
27
- const lines = [`== ${result.name} ==`];
32
+ export function formatEngineSection(
33
+ result: EngineResult,
34
+ paint: Painter = plainPainter,
35
+ ): string {
36
+ const lines = [paint(`== ${result.name} ==`, "header")];
28
37
  for (const skipped of result.skipped) {
29
- lines.push(`${result.name}: ${skipped}`);
38
+ lines.push(paint(`${result.name}: ${skipped}`, "muted"));
30
39
  }
31
40
  if (result.output !== "" && !isFindingEngine(result.name)) {
32
41
  lines.push(result.output);
33
42
  }
34
- for (const finding of [...result.reported, ...result.warnings]) {
35
- lines.push(describeFinding(finding));
43
+ for (const finding of result.reported) {
44
+ lines.push(describeFinding(finding, paint, "error"));
45
+ }
46
+ for (const finding of result.warnings) {
47
+ lines.push(describeFinding(finding, paint, "warn"));
36
48
  }
37
49
  if (result.message !== undefined) {
38
- lines.push(result.message);
50
+ lines.push(paint(result.message, "error"));
39
51
  }
40
- lines.push(`${result.name}: ${describeStatus(result)}`);
52
+ lines.push(`${result.name}: ${describeStatus(result, paint)}`);
41
53
  return lines.join("\n");
42
54
  }
43
55
 
44
56
  /**
45
57
  * 失敗したengineを列挙した集約summaryを組み立てる
46
58
  */
47
- export function formatSummary(results: EngineResult[]): string {
59
+ export function formatSummary(
60
+ results: EngineResult[],
61
+ paint: Painter = plainPainter,
62
+ ): string {
48
63
  const failed = results
49
64
  .filter((result) => result.status !== "passed")
50
65
  .map((result) => result.name);
@@ -52,14 +67,17 @@ export function formatSummary(results: EngineResult[]): string {
52
67
  .filter((result) => result.status === "passed")
53
68
  .map((result) => result.name);
54
69
  if (failed.length === 0) {
55
- return `quality-check: ${results.length} engines passed`;
70
+ return paint(`quality-check: ${results.length} engines passed`, "pass");
56
71
  }
57
72
  const lines = [
58
- `quality-check: ${failed.length} of ${results.length} engines failed`,
59
- ` failed: ${failed.join(", ")}`,
73
+ paint(
74
+ `quality-check: ${failed.length} of ${results.length} engines failed`,
75
+ "error",
76
+ ),
77
+ ` ${paint("failed:", "error")} ${failed.join(", ")}`,
60
78
  ];
61
79
  if (passed.length > 0) {
62
- lines.push(` passed: ${passed.join(", ")}`);
80
+ lines.push(` ${paint("passed:", "pass")} ${passed.join(", ")}`);
63
81
  }
64
82
  return lines.join("\n");
65
83
  }
package/src/run.ts CHANGED
@@ -44,6 +44,8 @@ export interface EngineResult {
44
44
  export interface RunOptions {
45
45
  /** 適用するbaseline nullなら差分判定を行わない */
46
46
  baseline: BaselineFile | null;
47
+ /** engine自身の出力へ色を付けるか */
48
+ color: boolean;
47
49
  config: QualityConfig;
48
50
  cwd: string;
49
51
  /** コマンドラインから渡された起動条件の上書き */
@@ -188,6 +190,7 @@ async function runEngine(
188
190
  export async function runEngines(options: RunOptions): Promise<EngineResult[]> {
189
191
  const runner = options.runner ?? runEngineProcess;
190
192
  const context: EngineCommandContext = {
193
+ color: options.color,
191
194
  config: options.config,
192
195
  overrides: options.overrides,
193
196
  rawBaseline: options.rawBaseline,