@yuu1111/quality-check 0.5.0 → 0.6.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/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@yuu1111/quality-check",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Integrated quality check runner",
5
- "license": "MIT",
6
5
  "repository": {
7
6
  "type": "git",
8
7
  "url": "git+https://github.com/yuu1111/configs.git",
@@ -10,7 +9,7 @@
10
9
  },
11
10
  "type": "module",
12
11
  "bin": {
13
- "quality-check": "src/cli.ts"
12
+ "quality-check": "dist/cli.js"
14
13
  },
15
14
  "exports": {
16
15
  ".": "./src/config.ts",
@@ -18,10 +17,18 @@
18
17
  },
19
18
  "files": [
20
19
  "README.ja.md",
21
- "src"
20
+ "src/config.ts",
21
+ "src/engines.ts",
22
+ "dist"
22
23
  ],
24
+ "scripts": {
25
+ "build": "bun build src/cli.ts --target=bun --outdir=dist"
26
+ },
23
27
  "keywords": [
24
28
  "quality",
25
29
  "lint"
26
- ]
30
+ ],
31
+ "devDependencies": {
32
+ "@yuu1111/shared": "workspace:*"
33
+ }
27
34
  }
package/src/config.ts CHANGED
@@ -39,12 +39,20 @@ export interface TsdocCheckOptions extends EngineOptions {
39
39
  error?: string[];
40
40
  }
41
41
 
42
+ /**
43
+ * 型検査へ渡す起動条件
44
+ */
45
+ export interface TypecheckOptions extends EngineOptions {
46
+ /** 型検査するtsconfigのpath 省略時はカレントのtsconfig.jsonを1回だけ読む */
47
+ projects?: string[];
48
+ }
49
+
42
50
  /**
43
51
  * engine名ごとの起動条件
44
52
  */
45
53
  export interface EngineConfigMap {
46
54
  biome: EngineOptions;
47
- typecheck: EngineOptions;
55
+ typecheck: TypecheckOptions;
48
56
  knip: EngineOptions;
49
57
  "comment-check": EngineOptions;
50
58
  "document-style-check": EngineOptions;
@@ -97,7 +105,7 @@ function isEngineName(value: string): value is EngineName {
97
105
 
98
106
  const ENGINE_OPTION_KEYS: Record<EngineName, readonly string[]> = {
99
107
  biome: ["args", "ignore", "targets"],
100
- typecheck: ["args", "ignore", "targets"],
108
+ typecheck: ["args", "ignore", "projects", "targets"],
101
109
  knip: ["args", "ignore", "targets"],
102
110
  "comment-check": ["args", "ignore", "targets"],
103
111
  "document-style-check": ["args", "ignore", "targets"],
@@ -155,11 +163,16 @@ function parseEngines(
155
163
  return engines;
156
164
  }
157
165
 
166
+ type ParsedEngineOptions = EngineOptions & {
167
+ error?: string[];
168
+ projects?: string[];
169
+ };
170
+
158
171
  function parseEngineOptions(
159
172
  value: JsonObject,
160
173
  source: string,
161
174
  name: EngineName,
162
- ): TsdocCheckOptions {
175
+ ): ParsedEngineOptions {
163
176
  const unknown = Object.keys(value).filter(
164
177
  (key) => !ENGINE_OPTION_KEYS[name].includes(key),
165
178
  );
@@ -168,7 +181,7 @@ function parseEngineOptions(
168
181
  `${source}: config.${name} has an unknown option: ${unknown[0]}`,
169
182
  );
170
183
  }
171
- const options: TsdocCheckOptions = {};
184
+ const options: ParsedEngineOptions = {};
172
185
  const ignore = readStringArray(
173
186
  value.ignore,
174
187
  `${source}: config.${name}.ignore`,
@@ -196,6 +209,18 @@ function parseEngineOptions(
196
209
  options.error = error;
197
210
  }
198
211
  }
212
+ if (name === "typecheck") {
213
+ const projects = readStringArray(
214
+ value.projects,
215
+ `${source}: config.${name}.projects`,
216
+ );
217
+ if (projects !== undefined) {
218
+ if (projects.length === 0) {
219
+ throw new Error(`${source}: config.${name}.projects must not be empty`);
220
+ }
221
+ options.projects = projects;
222
+ }
223
+ }
199
224
  return options;
200
225
  }
201
226
 
package/src/engines.ts CHANGED
@@ -70,7 +70,7 @@ export const ENGINE_LIMITS: Record<
70
70
  biome: { ignore: "biome.json holds its settings" },
71
71
  typecheck: {
72
72
  ignore: "tsconfig.json holds its settings",
73
- targets: "tsc checks the project named by tsconfig.json",
73
+ targets: "tsconfig.json and projects hold its settings",
74
74
  },
75
75
  knip: {
76
76
  ignore: "knip.ts holds its settings",
@@ -161,6 +161,24 @@ function colorArguments(name: EngineName, color: boolean): string[] {
161
161
  return [];
162
162
  }
163
163
 
164
+ /**
165
+ * 型検査のコマンドを組み立てる projectを渡すとそのtsconfigを読む
166
+ */
167
+ function buildTypecheckCommand(
168
+ executable: string,
169
+ context: EngineCommandContext,
170
+ project: string | undefined,
171
+ ): string[] {
172
+ const options = engineConfig(context.config, "typecheck") ?? {};
173
+ return [
174
+ executable,
175
+ "--noEmit",
176
+ ...(project === undefined ? [] : ["--project", project]),
177
+ ...colorArguments("typecheck", context.color),
178
+ ...(options.args ?? []),
179
+ ];
180
+ }
181
+
164
182
  /**
165
183
  * engineへ渡すコマンドを組み立てる
166
184
  */
@@ -194,12 +212,7 @@ export function buildEngineCommand(
194
212
  ];
195
213
  }
196
214
  if (name === "typecheck") {
197
- return [
198
- executable,
199
- "--noEmit",
200
- ...colorArguments(name, context.color),
201
- ...extra,
202
- ];
215
+ return buildTypecheckCommand(executable, context, undefined);
203
216
  }
204
217
  if (name === "knip") {
205
218
  return [executable, ...extra];
@@ -235,6 +248,26 @@ export function buildEngineCommand(
235
248
  ];
236
249
  }
237
250
 
251
+ /**
252
+ * engineの起動コマンドを順番に返す 型検査だけはprojectsごとに起動する
253
+ */
254
+ export function buildEngineCommands(
255
+ name: EngineName,
256
+ executable: string,
257
+ context: EngineCommandContext,
258
+ ): string[][] {
259
+ const projects =
260
+ name === "typecheck"
261
+ ? (engineConfig(context.config, "typecheck")?.projects ?? [])
262
+ : [];
263
+ if (projects.length === 0) {
264
+ return [buildEngineCommand(name, executable, context)];
265
+ }
266
+ return projects.map((project) =>
267
+ buildTypecheckCommand(executable, context, project),
268
+ );
269
+ }
270
+
238
271
  /**
239
272
  * Bun.spawnでengineを起動する既定のrunner
240
273
  */
package/src/baseline.ts DELETED
@@ -1,147 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import type { NormalizedFinding } from "./findings";
3
-
4
- /**
5
- * baselineへ記録する検出1件の識別情報と件数
6
- */
7
- export interface BaselineEntry {
8
- count: number;
9
- engine: string;
10
- file: string;
11
- rule: string;
12
- text: string;
13
- }
14
-
15
- /**
16
- * baseline fileの形式
17
- */
18
- export interface BaselineFile {
19
- entries: BaselineEntry[];
20
- version: 1;
21
- }
22
-
23
- /**
24
- * baselineと現在の検出を比較した結果
25
- */
26
- export interface BaselineComparison {
27
- added: NormalizedFinding[];
28
- resolved: BaselineEntry[];
29
- }
30
-
31
- type JsonObject = Record<string, unknown>;
32
-
33
- /**
34
- * 検出をbaseline上で一意に識別するkeyに使う項目
35
- */
36
- export interface BaselineKey {
37
- engine: string;
38
- file: string;
39
- rule: string;
40
- text: string;
41
- }
42
-
43
- function isJsonObject(value: unknown): value is JsonObject {
44
- return typeof value === "object" && value !== null && !Array.isArray(value);
45
- }
46
-
47
- function isBaselineEntry(value: unknown): value is BaselineEntry {
48
- if (!isJsonObject(value)) {
49
- return false;
50
- }
51
- return (
52
- typeof value.engine === "string" &&
53
- typeof value.rule === "string" &&
54
- typeof value.file === "string" &&
55
- typeof value.text === "string" &&
56
- typeof value.count === "number"
57
- );
58
- }
59
-
60
- function entryKey(entry: BaselineKey): string {
61
- return [entry.engine, entry.rule, entry.file, entry.text].join("\u0000");
62
- }
63
-
64
- function compareEntries(left: BaselineEntry, right: BaselineEntry): number {
65
- const leftKey = entryKey(left);
66
- const rightKey = entryKey(right);
67
- if (leftKey === rightKey) {
68
- return 0;
69
- }
70
- return leftKey < rightKey ? -1 : 1;
71
- }
72
-
73
- /**
74
- * 検出をengineとruleとfileと本文で集計してbaselineを作る
75
- */
76
- export function createBaseline(findings: NormalizedFinding[]): BaselineFile {
77
- const entries = new Map<string, BaselineEntry>();
78
- for (const finding of findings) {
79
- const key = entryKey(finding);
80
- const existing = entries.get(key);
81
- if (existing) {
82
- existing.count += 1;
83
- continue;
84
- }
85
- entries.set(key, {
86
- count: 1,
87
- engine: finding.engine,
88
- file: finding.file,
89
- rule: finding.rule,
90
- text: finding.text,
91
- });
92
- }
93
- return { entries: [...entries.values()].sort(compareEntries), version: 1 };
94
- }
95
-
96
- /**
97
- * 現在の検出からbaseline済みを除き、解消済みentryを求める
98
- */
99
- export function compareWithBaseline(
100
- findings: NormalizedFinding[],
101
- baseline: BaselineFile,
102
- ): BaselineComparison {
103
- const remaining = new Map<string, number>();
104
- for (const entry of baseline.entries) {
105
- const key = entryKey(entry);
106
- remaining.set(key, (remaining.get(key) ?? 0) + entry.count);
107
- }
108
- const added: NormalizedFinding[] = [];
109
- for (const finding of findings) {
110
- const key = entryKey(finding);
111
- const count = remaining.get(key) ?? 0;
112
- if (count > 0) {
113
- remaining.set(key, count - 1);
114
- continue;
115
- }
116
- added.push(finding);
117
- }
118
- const resolved = baseline.entries.filter(
119
- (entry) => (remaining.get(entryKey(entry)) ?? 0) > 0,
120
- );
121
- return { added, resolved };
122
- }
123
-
124
- /**
125
- * baseline fileを読み込む 存在しない場合は空のbaselineを返す
126
- */
127
- export function readBaseline(path: string): BaselineFile {
128
- if (!existsSync(path)) {
129
- return { entries: [], version: 1 };
130
- }
131
- const value: unknown = JSON.parse(readFileSync(path, "utf8"));
132
- if (
133
- !isJsonObject(value) ||
134
- !Array.isArray(value.entries) ||
135
- !value.entries.every(isBaselineEntry)
136
- ) {
137
- throw new Error(`${path} is not a quality baseline`);
138
- }
139
- return { entries: value.entries, version: 1 };
140
- }
141
-
142
- /**
143
- * baseline fileをタブ区切りのJSONで書き込む
144
- */
145
- export function writeBaseline(path: string, baseline: BaselineFile): void {
146
- writeFileSync(path, `${JSON.stringify(baseline, null, "\t")}\n`);
147
- }
package/src/cli.ts DELETED
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { tmpdir } from "node:os";
3
- import { join, resolve } from "node:path";
4
- import { createBaseline, readBaseline, writeBaseline } from "./baseline";
5
- import { ansiPainter, colorEnabled, plainPainter } from "./color";
6
- import {
7
- DEFAULT_BASELINE_FILE,
8
- findConfigFile,
9
- loadConfig,
10
- type QualityConfig,
11
- } from "./config";
12
- import { formatEngineSection, formatSummary, toJsonReport } from "./report";
13
- import { runEngines } from "./run";
14
-
15
- const USAGE =
16
- "Usage: quality-check [--config <path>] [--baseline <path>] [--ignore <path>] [--update-baseline] [--json] [path...]";
17
-
18
- interface Options {
19
- baselinePath: string | undefined;
20
- configPath: string | undefined;
21
- ignores: string[];
22
- json: boolean;
23
- targets: string[];
24
- update: boolean;
25
- }
26
-
27
- function applyFlagOption(options: Options, argument: string): boolean {
28
- if (argument === "--json") {
29
- options.json = true;
30
- return true;
31
- }
32
- if (argument === "--update-baseline") {
33
- options.update = true;
34
- return true;
35
- }
36
- return false;
37
- }
38
-
39
- function applyValueOption(
40
- options: Options,
41
- argument: string,
42
- argv: string[],
43
- index: number,
44
- ): number | null {
45
- if (argument === "--config") {
46
- options.configPath = argv[index + 1];
47
- return 1;
48
- }
49
- if (argument.startsWith("--config=")) {
50
- options.configPath = argument.slice("--config=".length);
51
- return 0;
52
- }
53
- if (argument === "--baseline") {
54
- options.baselinePath = argv[index + 1];
55
- return 1;
56
- }
57
- if (argument.startsWith("--baseline=")) {
58
- options.baselinePath = argument.slice("--baseline=".length);
59
- return 0;
60
- }
61
- if (argument === "--ignore") {
62
- const value = argv[index + 1];
63
- if (value !== undefined) {
64
- options.ignores.push(value);
65
- }
66
- return 1;
67
- }
68
- if (argument.startsWith("--ignore=")) {
69
- options.ignores.push(argument.slice("--ignore=".length));
70
- return 0;
71
- }
72
- return null;
73
- }
74
-
75
- function parseArguments(argv: string[]): Options {
76
- const options: Options = {
77
- baselinePath: undefined,
78
- configPath: undefined,
79
- ignores: [],
80
- json: false,
81
- targets: [],
82
- update: false,
83
- };
84
- for (let index = 0; index < argv.length; index += 1) {
85
- const argument = argv[index] ?? "";
86
- const consumed = applyValueOption(options, argument, argv, index);
87
- if (consumed !== null) {
88
- index += consumed;
89
- continue;
90
- }
91
- if (applyFlagOption(options, argument)) {
92
- continue;
93
- }
94
- if (argument.startsWith("-")) {
95
- throw new Error(`unknown option: ${argument}`);
96
- }
97
- options.targets.push(argument);
98
- }
99
- return options;
100
- }
101
-
102
- function resolveBaselinePath(
103
- options: Options,
104
- config: QualityConfig,
105
- cwd: string,
106
- ): string | null {
107
- if (options.baselinePath !== undefined) {
108
- return resolve(cwd, options.baselinePath);
109
- }
110
- if (config.baseline === false) {
111
- return null;
112
- }
113
- return resolve(cwd, config.baseline ?? DEFAULT_BASELINE_FILE);
114
- }
115
-
116
- function resolveConfigPath(options: Options, cwd: string): string | null {
117
- if (options.configPath !== undefined) {
118
- return resolve(cwd, options.configPath);
119
- }
120
- return findConfigFile(cwd);
121
- }
122
-
123
- async function main(argv: string[]): Promise<number> {
124
- if (argv.includes("--help") || argv.includes("-h")) {
125
- console.log(USAGE);
126
- return 0;
127
- }
128
- const options = parseArguments(argv);
129
- const color = colorEnabled(process.stdout, process.env);
130
- const paint = color ? ansiPainter() : plainPainter;
131
- const cwd = process.cwd();
132
- const configPath = resolveConfigPath(options, cwd);
133
- if (configPath === null) {
134
- console.error(`quality.config.ts not found in ${cwd}`);
135
- return 2;
136
- }
137
- const config = await loadConfig(configPath);
138
- const baselinePath = resolveBaselinePath(options, config, cwd);
139
- const results = await runEngines({
140
- baseline:
141
- options.update || baselinePath === null
142
- ? null
143
- : readBaseline(baselinePath),
144
- color,
145
- config,
146
- cwd,
147
- overrides: { ignore: options.ignores, targets: options.targets },
148
- rawBaseline: join(tmpdir(), `quality-check-raw-${process.pid}.json`),
149
- });
150
- if (options.update) {
151
- if (baselinePath === null) {
152
- console.error("baseline is disabled by the configuration");
153
- return 2;
154
- }
155
- const baseline = createBaseline(
156
- results.flatMap((result) => result.detected),
157
- );
158
- writeBaseline(baselinePath, baseline);
159
- console.log(
160
- paint(
161
- `Recorded ${baseline.entries.length} entries in ${baselinePath}`,
162
- "pass",
163
- ),
164
- );
165
- return 0;
166
- }
167
- if (options.json) {
168
- console.log(JSON.stringify(toJsonReport(results), null, "\t"));
169
- } else {
170
- for (const result of results) {
171
- console.log(formatEngineSection(result, paint));
172
- }
173
- console.log(formatSummary(results, paint));
174
- }
175
- return results.some((result) => result.status !== "passed") ? 1 : 0;
176
- }
177
-
178
- try {
179
- process.exit(await main(process.argv.slice(2)));
180
- } catch (error) {
181
- console.error(error instanceof Error ? error.message : String(error));
182
- process.exit(2);
183
- }
package/src/color.ts DELETED
@@ -1,47 +0,0 @@
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/findings.ts DELETED
@@ -1,106 +0,0 @@
1
- import type { EngineName } from "./config";
2
-
3
- /**
4
- * 検出の重大度
5
- */
6
- export type FindingSeverity = "error" | "warning";
7
-
8
- /**
9
- * engine間の差を吸収した検出1件
10
- */
11
- export interface NormalizedFinding {
12
- column: number;
13
- engine: EngineName;
14
- file: string;
15
- line: number;
16
- rule: string;
17
- severity: FindingSeverity;
18
- text: string;
19
- }
20
-
21
- /**
22
- * engineが--jsonで返した検出の分類
23
- */
24
- export interface ParsedFindings {
25
- errors: NormalizedFinding[];
26
- warnings: NormalizedFinding[];
27
- }
28
-
29
- type JsonObject = Record<string, unknown>;
30
-
31
- function isJsonObject(value: unknown): value is JsonObject {
32
- return typeof value === "object" && value !== null && !Array.isArray(value);
33
- }
34
-
35
- function readFinding(
36
- engine: EngineName,
37
- value: unknown,
38
- severity: FindingSeverity,
39
- textField: "message" | "text",
40
- ): NormalizedFinding {
41
- if (!isJsonObject(value)) {
42
- throw new Error(`${engine} printed an unexpected finding`);
43
- }
44
- const { column, file, line, rule } = value;
45
- const text = value[textField];
46
- if (
47
- typeof rule !== "string" ||
48
- typeof file !== "string" ||
49
- typeof line !== "number" ||
50
- typeof column !== "number" ||
51
- typeof text !== "string"
52
- ) {
53
- throw new Error(`${engine} printed an unexpected finding`);
54
- }
55
- return { column, engine, file, line, rule, severity, text };
56
- }
57
-
58
- function readEntries(
59
- engine: EngineName,
60
- value: unknown,
61
- field: string,
62
- ): unknown[] {
63
- if (!Array.isArray(value)) {
64
- throw new Error(`${engine} printed JSON without a ${field} array`);
65
- }
66
- return value;
67
- }
68
-
69
- function parseJson(engine: EngineName, stdout: string): JsonObject {
70
- let value: unknown;
71
- try {
72
- value = JSON.parse(stdout);
73
- } catch {
74
- throw new Error(`${engine} did not print JSON`);
75
- }
76
- if (!isJsonObject(value)) {
77
- throw new Error(`${engine} printed an unexpected JSON value`);
78
- }
79
- return value;
80
- }
81
-
82
- /**
83
- * engineの--json出力を検出へ変換する 解析できない出力は例外にする
84
- */
85
- export function parseFindings(
86
- engine: EngineName,
87
- stdout: string,
88
- ): ParsedFindings {
89
- const value = parseJson(engine, stdout);
90
- if (engine === "comment-check") {
91
- return {
92
- errors: readEntries(engine, value.added, "added").map((entry) =>
93
- readFinding(engine, entry, "error", "text"),
94
- ),
95
- warnings: [],
96
- };
97
- }
98
- return {
99
- errors: readEntries(engine, value.errors, "errors").map((entry) =>
100
- readFinding(engine, entry, "error", "message"),
101
- ),
102
- warnings: readEntries(engine, value.warnings, "warnings").map((entry) =>
103
- readFinding(engine, entry, "warning", "message"),
104
- ),
105
- };
106
- }