@yuu1111/quality-check 0.5.1 → 0.7.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 +7 -7
- package/README.md +13 -9
- package/dist/cli.js +861 -0
- package/package.json +12 -5
- package/src/config.ts +81 -18
- package/src/engines.ts +51 -7
- package/src/baseline.ts +0 -147
- package/src/cli.ts +0 -183
- package/src/color.ts +0 -47
- package/src/findings.ts +0 -106
- package/src/report.ts +0 -105
- package/src/run.ts +0 -203
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuu1111/quality-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": "
|
|
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
|
@@ -31,6 +31,22 @@ export interface EngineOptions {
|
|
|
31
31
|
args?: string[];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* comment-checkへ渡す起動条件
|
|
36
|
+
*/
|
|
37
|
+
export interface CommentCheckOptions extends EngineOptions {
|
|
38
|
+
/** 既定で無効のopt-in ruleのうち有効にするrule名 */
|
|
39
|
+
enable?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* document-style-checkへ渡す起動条件
|
|
44
|
+
*/
|
|
45
|
+
export interface DocumentStyleCheckOptions extends EngineOptions {
|
|
46
|
+
/** 既定で無効のopt-in ruleのうち有効にするrule名 */
|
|
47
|
+
enable?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
/**
|
|
35
51
|
* TSDoc検査へ渡す起動条件
|
|
36
52
|
*/
|
|
@@ -39,15 +55,23 @@ export interface TsdocCheckOptions extends EngineOptions {
|
|
|
39
55
|
error?: string[];
|
|
40
56
|
}
|
|
41
57
|
|
|
58
|
+
/**
|
|
59
|
+
* 型検査へ渡す起動条件
|
|
60
|
+
*/
|
|
61
|
+
export interface TypecheckOptions extends EngineOptions {
|
|
62
|
+
/** 型検査するtsconfigのpath 省略時はカレントのtsconfig.jsonを1回だけ読む */
|
|
63
|
+
projects?: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
/**
|
|
43
67
|
* engine名ごとの起動条件
|
|
44
68
|
*/
|
|
45
69
|
export interface EngineConfigMap {
|
|
46
70
|
biome: EngineOptions;
|
|
47
|
-
typecheck:
|
|
71
|
+
typecheck: TypecheckOptions;
|
|
48
72
|
knip: EngineOptions;
|
|
49
|
-
"comment-check":
|
|
50
|
-
"document-style-check":
|
|
73
|
+
"comment-check": CommentCheckOptions;
|
|
74
|
+
"document-style-check": DocumentStyleCheckOptions;
|
|
51
75
|
"tsdoc-check": TsdocCheckOptions;
|
|
52
76
|
}
|
|
53
77
|
|
|
@@ -97,10 +121,10 @@ function isEngineName(value: string): value is EngineName {
|
|
|
97
121
|
|
|
98
122
|
const ENGINE_OPTION_KEYS: Record<EngineName, readonly string[]> = {
|
|
99
123
|
biome: ["args", "ignore", "targets"],
|
|
100
|
-
typecheck: ["args", "ignore", "targets"],
|
|
124
|
+
typecheck: ["args", "ignore", "projects", "targets"],
|
|
101
125
|
knip: ["args", "ignore", "targets"],
|
|
102
|
-
"comment-check": ["args", "ignore", "targets"],
|
|
103
|
-
"document-style-check": ["args", "ignore", "targets"],
|
|
126
|
+
"comment-check": ["args", "enable", "ignore", "targets"],
|
|
127
|
+
"document-style-check": ["args", "enable", "ignore", "targets"],
|
|
104
128
|
"tsdoc-check": ["args", "error", "ignore", "targets"],
|
|
105
129
|
};
|
|
106
130
|
|
|
@@ -155,11 +179,59 @@ function parseEngines(
|
|
|
155
179
|
return engines;
|
|
156
180
|
}
|
|
157
181
|
|
|
182
|
+
type ParsedEngineOptions = EngineOptions & {
|
|
183
|
+
enable?: string[];
|
|
184
|
+
error?: string[];
|
|
185
|
+
projects?: string[];
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* engine名ごとにしか受け取らない起動条件を読み取る
|
|
190
|
+
*/
|
|
191
|
+
function parseEngineExtras(
|
|
192
|
+
value: JsonObject,
|
|
193
|
+
source: string,
|
|
194
|
+
name: EngineName,
|
|
195
|
+
): ParsedEngineOptions {
|
|
196
|
+
const extras: ParsedEngineOptions = {};
|
|
197
|
+
if (name === "comment-check" || name === "document-style-check") {
|
|
198
|
+
const enable = readStringArray(
|
|
199
|
+
value.enable,
|
|
200
|
+
`${source}: config.${name}.enable`,
|
|
201
|
+
);
|
|
202
|
+
if (enable !== undefined) {
|
|
203
|
+
extras.enable = enable;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (name === "tsdoc-check") {
|
|
207
|
+
const error = readStringArray(
|
|
208
|
+
value.error,
|
|
209
|
+
`${source}: config.${name}.error`,
|
|
210
|
+
);
|
|
211
|
+
if (error !== undefined) {
|
|
212
|
+
extras.error = error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (name === "typecheck") {
|
|
216
|
+
const projects = readStringArray(
|
|
217
|
+
value.projects,
|
|
218
|
+
`${source}: config.${name}.projects`,
|
|
219
|
+
);
|
|
220
|
+
if (projects !== undefined) {
|
|
221
|
+
if (projects.length === 0) {
|
|
222
|
+
throw new Error(`${source}: config.${name}.projects must not be empty`);
|
|
223
|
+
}
|
|
224
|
+
extras.projects = projects;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return extras;
|
|
228
|
+
}
|
|
229
|
+
|
|
158
230
|
function parseEngineOptions(
|
|
159
231
|
value: JsonObject,
|
|
160
232
|
source: string,
|
|
161
233
|
name: EngineName,
|
|
162
|
-
):
|
|
234
|
+
): ParsedEngineOptions {
|
|
163
235
|
const unknown = Object.keys(value).filter(
|
|
164
236
|
(key) => !ENGINE_OPTION_KEYS[name].includes(key),
|
|
165
237
|
);
|
|
@@ -168,7 +240,7 @@ function parseEngineOptions(
|
|
|
168
240
|
`${source}: config.${name} has an unknown option: ${unknown[0]}`,
|
|
169
241
|
);
|
|
170
242
|
}
|
|
171
|
-
const options:
|
|
243
|
+
const options: ParsedEngineOptions = {};
|
|
172
244
|
const ignore = readStringArray(
|
|
173
245
|
value.ignore,
|
|
174
246
|
`${source}: config.${name}.ignore`,
|
|
@@ -187,16 +259,7 @@ function parseEngineOptions(
|
|
|
187
259
|
if (args !== undefined) {
|
|
188
260
|
options.args = args;
|
|
189
261
|
}
|
|
190
|
-
|
|
191
|
-
const error = readStringArray(
|
|
192
|
-
value.error,
|
|
193
|
-
`${source}: config.${name}.error`,
|
|
194
|
-
);
|
|
195
|
-
if (error !== undefined) {
|
|
196
|
-
options.error = error;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return options;
|
|
262
|
+
return { ...options, ...parseEngineExtras(value, source, name) };
|
|
200
263
|
}
|
|
201
264
|
|
|
202
265
|
function parseEngineConfig(
|
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: "
|
|
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,33 @@ 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
|
+
|
|
182
|
+
/**
|
|
183
|
+
* opt-in ruleを有効にするコマンド引数へ展開する
|
|
184
|
+
*/
|
|
185
|
+
function enableArguments(
|
|
186
|
+
options: { enable?: string[] } | null | undefined,
|
|
187
|
+
): string[] {
|
|
188
|
+
return (options?.enable ?? []).flatMap((rule) => ["--enable", rule]);
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
/**
|
|
165
192
|
* engineへ渡すコマンドを組み立てる
|
|
166
193
|
*/
|
|
@@ -194,12 +221,7 @@ export function buildEngineCommand(
|
|
|
194
221
|
];
|
|
195
222
|
}
|
|
196
223
|
if (name === "typecheck") {
|
|
197
|
-
return
|
|
198
|
-
executable,
|
|
199
|
-
"--noEmit",
|
|
200
|
-
...colorArguments(name, context.color),
|
|
201
|
-
...extra,
|
|
202
|
-
];
|
|
224
|
+
return buildTypecheckCommand(executable, context, undefined);
|
|
203
225
|
}
|
|
204
226
|
if (name === "knip") {
|
|
205
227
|
return [executable, ...extra];
|
|
@@ -210,6 +232,7 @@ export function buildEngineCommand(
|
|
|
210
232
|
"--json",
|
|
211
233
|
"--baseline",
|
|
212
234
|
context.rawBaseline,
|
|
235
|
+
...enableArguments(engineConfig(context.config, "comment-check")),
|
|
213
236
|
...targets,
|
|
214
237
|
...ignoreArguments,
|
|
215
238
|
...extra,
|
|
@@ -220,6 +243,7 @@ export function buildEngineCommand(
|
|
|
220
243
|
executable,
|
|
221
244
|
"lint",
|
|
222
245
|
"--json",
|
|
246
|
+
...enableArguments(engineConfig(context.config, "document-style-check")),
|
|
223
247
|
...targets,
|
|
224
248
|
...ignoreArguments,
|
|
225
249
|
...extra,
|
|
@@ -235,6 +259,26 @@ export function buildEngineCommand(
|
|
|
235
259
|
];
|
|
236
260
|
}
|
|
237
261
|
|
|
262
|
+
/**
|
|
263
|
+
* engineの起動コマンドを順番に返す 型検査だけはprojectsごとに起動する
|
|
264
|
+
*/
|
|
265
|
+
export function buildEngineCommands(
|
|
266
|
+
name: EngineName,
|
|
267
|
+
executable: string,
|
|
268
|
+
context: EngineCommandContext,
|
|
269
|
+
): string[][] {
|
|
270
|
+
const projects =
|
|
271
|
+
name === "typecheck"
|
|
272
|
+
? (engineConfig(context.config, "typecheck")?.projects ?? [])
|
|
273
|
+
: [];
|
|
274
|
+
if (projects.length === 0) {
|
|
275
|
+
return [buildEngineCommand(name, executable, context)];
|
|
276
|
+
}
|
|
277
|
+
return projects.map((project) =>
|
|
278
|
+
buildTypecheckCommand(executable, context, project),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
238
282
|
/**
|
|
239
283
|
* Bun.spawnでengineを起動する既定のrunner
|
|
240
284
|
*/
|
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
|
-
}
|