fenge 0.10.0 → 0.10.2
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 +3 -3
- package/src/bin/fenge.cli.js +29 -6
- package/src/command/format.js +5 -6
- package/src/command/lint.js +42 -5
- package/src/utils.js +59 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fenge",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "A CLI tool for code quality",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"prettier": "3.6.2",
|
|
52
52
|
"pretty-ms": "9.2.0",
|
|
53
53
|
"yoctocolors": "2.1.2",
|
|
54
|
-
"@fenge/eslint-config": "0.7.
|
|
55
|
-
"@fenge/prettier-config": "0.3.
|
|
54
|
+
"@fenge/eslint-config": "0.7.12",
|
|
55
|
+
"@fenge/prettier-config": "0.3.10",
|
|
56
56
|
"@fenge/tsconfig": "0.7.0",
|
|
57
57
|
"prettier-ignore": "0.4.0"
|
|
58
58
|
},
|
package/src/bin/fenge.cli.js
CHANGED
|
@@ -35,14 +35,27 @@ program
|
|
|
35
35
|
)
|
|
36
36
|
.argument("[paths...]", "dir or file paths to format and lint")
|
|
37
37
|
.action(async (paths, options) => {
|
|
38
|
+
/**
|
|
39
|
+
* @type {{code: number, stdout: string, stderr: string, fixedFiles?: string[]}}
|
|
40
|
+
*/
|
|
38
41
|
let result = await format(paths, options);
|
|
39
|
-
|
|
42
|
+
result.stdout && console.log(result.stdout);
|
|
43
|
+
result.stderr && console.error(result.stderr);
|
|
44
|
+
if (result.code === 0) {
|
|
40
45
|
result = await lint(paths, options);
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
result.stdout && console.log(result.stdout);
|
|
47
|
+
result.stderr && console.error(result.stderr);
|
|
48
|
+
if (
|
|
49
|
+
result.code === 0 &&
|
|
50
|
+
(options.fix || options.update) &&
|
|
51
|
+
result.fixedFiles?.length
|
|
52
|
+
) {
|
|
53
|
+
result = await format(result.fixedFiles, options);
|
|
54
|
+
result.stdout && console.log(result.stdout);
|
|
55
|
+
result.stderr && console.error(result.stderr);
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
|
-
process.exit(result);
|
|
58
|
+
process.exit(result.code);
|
|
46
59
|
});
|
|
47
60
|
|
|
48
61
|
program
|
|
@@ -61,7 +74,12 @@ program
|
|
|
61
74
|
"print what command will be executed under the hood instead of executing",
|
|
62
75
|
)
|
|
63
76
|
.argument("[paths...]", "dir or file paths to lint")
|
|
64
|
-
.action(async (paths, options) =>
|
|
77
|
+
.action(async (paths, options) => {
|
|
78
|
+
const { code, stdout, stderr } = await lint(paths, options);
|
|
79
|
+
stdout && console.log(stdout);
|
|
80
|
+
stderr && console.error(stderr);
|
|
81
|
+
process.exit(code);
|
|
82
|
+
});
|
|
65
83
|
|
|
66
84
|
program
|
|
67
85
|
.command("format")
|
|
@@ -78,7 +96,12 @@ program
|
|
|
78
96
|
"print what command will be executed under the hood instead of executing",
|
|
79
97
|
)
|
|
80
98
|
.argument("[paths...]", "dir or file paths to format")
|
|
81
|
-
.action(async (paths, options) =>
|
|
99
|
+
.action(async (paths, options) => {
|
|
100
|
+
const { code, stdout, stderr } = await format(paths, options);
|
|
101
|
+
stdout && console.log(stdout);
|
|
102
|
+
stderr && console.error(stderr);
|
|
103
|
+
process.exit(code);
|
|
104
|
+
});
|
|
82
105
|
|
|
83
106
|
program
|
|
84
107
|
.command("install")
|
package/src/command/format.js
CHANGED
|
@@ -18,8 +18,9 @@ export async function format(
|
|
|
18
18
|
default: useDefaultConfig = false,
|
|
19
19
|
} = {},
|
|
20
20
|
) {
|
|
21
|
-
return execAsync(
|
|
21
|
+
return execAsync("💃 Checking formatting")(
|
|
22
22
|
[
|
|
23
|
+
process.execPath,
|
|
23
24
|
await getPrettierPath(useDefaultConfig),
|
|
24
25
|
// setup 3 ignore files
|
|
25
26
|
...[".gitignore", ".prettierignore", prettierignore]
|
|
@@ -37,7 +38,6 @@ export async function format(
|
|
|
37
38
|
),
|
|
38
39
|
],
|
|
39
40
|
{
|
|
40
|
-
topic: "💃 Checking formatting",
|
|
41
41
|
dryRun,
|
|
42
42
|
env: {
|
|
43
43
|
...(config && { FENGE_CONFIG: config }),
|
|
@@ -51,11 +51,10 @@ export async function format(
|
|
|
51
51
|
* @param {boolean} useDefaultConfig
|
|
52
52
|
*/
|
|
53
53
|
async function getPrettierPath(useDefaultConfig) {
|
|
54
|
-
const builtinBinPath = await getBinPath("prettier");
|
|
55
54
|
if (useDefaultConfig) {
|
|
56
|
-
return
|
|
55
|
+
return await getBinPath("prettier");
|
|
57
56
|
}
|
|
58
|
-
return await getBinPath("prettier", process.cwd()).catch(
|
|
59
|
-
()
|
|
57
|
+
return await getBinPath("prettier", process.cwd()).catch(() =>
|
|
58
|
+
getBinPath("prettier"),
|
|
60
59
|
);
|
|
61
60
|
}
|
package/src/command/lint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { ESLint } from "eslint";
|
|
4
5
|
import { dir, execAsync, getBinPath } from "../utils.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -18,9 +19,11 @@ export async function lint(
|
|
|
18
19
|
timing = false,
|
|
19
20
|
} = {},
|
|
20
21
|
) {
|
|
21
|
-
|
|
22
|
+
const result = await execAsync("📏 Checking linting")(
|
|
22
23
|
[
|
|
24
|
+
process.execPath,
|
|
23
25
|
await getEslintPath(useDefaultConfig),
|
|
26
|
+
...(timing ? [] : ["--format=json"]),
|
|
24
27
|
"--config",
|
|
25
28
|
path.join(dir(import.meta.url), "..", "config", "eslint.config.js"),
|
|
26
29
|
...(update || fix ? ["--fix"] : []),
|
|
@@ -29,7 +32,6 @@ export async function lint(
|
|
|
29
32
|
),
|
|
30
33
|
],
|
|
31
34
|
{
|
|
32
|
-
topic: "📏 Checking linting",
|
|
33
35
|
dryRun,
|
|
34
36
|
env: {
|
|
35
37
|
...(config && { FENGE_CONFIG: config }),
|
|
@@ -38,15 +40,50 @@ export async function lint(
|
|
|
38
40
|
},
|
|
39
41
|
},
|
|
40
42
|
);
|
|
43
|
+
// Loading formatter in this way is not very elegant, but it's the only way (maybe).
|
|
44
|
+
const formatter = await new ESLint().loadFormatter("stylish");
|
|
45
|
+
const stdoutResult = await handleJson(formatter, result.stdout);
|
|
46
|
+
const stderrResult = await handleJson(formatter, result.stderr);
|
|
47
|
+
return {
|
|
48
|
+
...result,
|
|
49
|
+
stdout: stdoutResult.content,
|
|
50
|
+
stderr: stderrResult.content,
|
|
51
|
+
fixedFiles: [
|
|
52
|
+
...new Set([...stdoutResult.fixedFiles, ...stderrResult.fixedFiles]),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {import('eslint').ESLint.LoadedFormatter} formatter
|
|
59
|
+
* @param {string} str
|
|
60
|
+
*/
|
|
61
|
+
async function handleJson(formatter, str) {
|
|
62
|
+
try {
|
|
63
|
+
/** @type {import('eslint').ESLint.LintResult[]} */
|
|
64
|
+
const lintResults = JSON.parse(str);
|
|
65
|
+
return {
|
|
66
|
+
fixedFiles: lintResults
|
|
67
|
+
.filter((lintResult) => lintResult.output)
|
|
68
|
+
.map((lintResult) => lintResult.filePath),
|
|
69
|
+
content: await formatter.format(lintResults),
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return {
|
|
73
|
+
fixedFiles: [],
|
|
74
|
+
content: str,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
41
77
|
}
|
|
42
78
|
|
|
43
79
|
/**
|
|
44
80
|
* @param {boolean} useDefaultConfig
|
|
45
81
|
*/
|
|
46
82
|
async function getEslintPath(useDefaultConfig) {
|
|
47
|
-
const builtinBinPath = await getBinPath("eslint");
|
|
48
83
|
if (useDefaultConfig) {
|
|
49
|
-
return
|
|
84
|
+
return await getBinPath("eslint");
|
|
50
85
|
}
|
|
51
|
-
return await getBinPath("eslint", process.cwd()).catch(() =>
|
|
86
|
+
return await getBinPath("eslint", process.cwd()).catch(() =>
|
|
87
|
+
getBinPath("eslint"),
|
|
88
|
+
);
|
|
52
89
|
}
|
package/src/utils.js
CHANGED
|
@@ -42,30 +42,65 @@ export async function resolveConfig(module, loadPath) {
|
|
|
42
42
|
: await searcher.search(process.cwd());
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Creates a spinner wrapper for async functions that shows progress and timing
|
|
47
|
+
* @template {readonly unknown[]} TArgs - The arguments type for the function
|
|
48
|
+
* @template {{ code: number }} TReturn - The return type that must have a code property
|
|
49
|
+
* @param {(...args: TArgs) => Promise<TReturn>} func - The async function to wrap
|
|
50
|
+
* @param {string} topic - The topic/description to show in the spinner
|
|
51
|
+
* @returns {(...args: TArgs) => Promise<TReturn>} The wrapped function with spinner
|
|
52
|
+
*/
|
|
53
|
+
export function spin(func, topic) {
|
|
54
|
+
return async (...args) => {
|
|
55
|
+
const startTime = Date.now();
|
|
56
|
+
const spinner = ora(`${topic}...`).start();
|
|
57
|
+
/** @type {"succeed" | "fail"} */
|
|
58
|
+
let succeedOrFail = "fail";
|
|
59
|
+
try {
|
|
60
|
+
const result = await func(...args);
|
|
61
|
+
if (result.code === 0) succeedOrFail = "succeed";
|
|
62
|
+
return result;
|
|
63
|
+
} finally {
|
|
64
|
+
spinner[succeedOrFail](
|
|
65
|
+
`${topic} ${succeedOrFail} in ${colors.yellow(prettyMs(Date.now() - startTime))}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
/**
|
|
46
72
|
* @param {string[]} command
|
|
47
|
-
* @param {{
|
|
48
|
-
* @returns {Promise<number>}
|
|
73
|
+
* @param {{dryRun: boolean, env: Record<string, string>}} options
|
|
74
|
+
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
49
75
|
*/
|
|
50
|
-
|
|
76
|
+
function execCmd(command, { dryRun, env }) {
|
|
51
77
|
const [cmd, ...args] = command;
|
|
52
78
|
if (!cmd) {
|
|
53
79
|
return Promise.reject(new Error("cmd not found"));
|
|
54
80
|
}
|
|
55
81
|
if (dryRun) {
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
return Promise.resolve({
|
|
83
|
+
code: 0,
|
|
84
|
+
stdout: `${colors.green(cmd)} ${args.join(" ")};\n`,
|
|
85
|
+
stderr: "",
|
|
86
|
+
});
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
return new Promise((resolve, reject) => {
|
|
61
|
-
const startTime = Date.now();
|
|
62
|
-
const spinner = ora(`${topic}...`).start();
|
|
63
90
|
/**
|
|
64
91
|
* @type {childProcess.ChildProcessWithoutNullStreams | undefined}
|
|
65
92
|
*/
|
|
66
93
|
let cp = childProcess.spawn(cmd, args, {
|
|
67
94
|
env: { FORCE_COLOR: "true", ...process.env, ...env },
|
|
68
95
|
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {NodeJS.Signals} signal
|
|
99
|
+
*/
|
|
100
|
+
const listener = (signal) => cp && !cp.killed && cp.kill(signal);
|
|
101
|
+
process.on("SIGINT", listener);
|
|
102
|
+
process.on("SIGTERM", listener);
|
|
103
|
+
|
|
69
104
|
let stdout = Buffer.alloc(0);
|
|
70
105
|
let stderr = Buffer.alloc(0);
|
|
71
106
|
cp.stdout.on("data", (data) => {
|
|
@@ -75,39 +110,37 @@ export function execAsync(command, { topic, dryRun, env }) {
|
|
|
75
110
|
stderr = Buffer.concat([stderr, data]);
|
|
76
111
|
});
|
|
77
112
|
cp.on("error", (err) => {
|
|
113
|
+
process.removeListener("SIGINT", listener);
|
|
114
|
+
process.removeListener("SIGTERM", listener);
|
|
78
115
|
reject(err);
|
|
79
116
|
});
|
|
80
117
|
// Why not listen to the 'exit' event?
|
|
81
118
|
// 1. The 'close' event will always emit after 'exit' was already emitted, or 'error' if the child failed to spawn.
|
|
82
119
|
// 2. The 'exit' event may or may not fire after an error has occurred.
|
|
83
120
|
cp.on("close", (code, signal) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`${topic} succeeded in ${colors.yellow(prettyMs(Date.now() - startTime))}`,
|
|
87
|
-
);
|
|
88
|
-
} else {
|
|
89
|
-
spinner.fail(
|
|
90
|
-
`${topic} failed in ${colors.yellow(prettyMs(Date.now() - startTime))}`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
process.stdout.write(stdout);
|
|
94
|
-
process.stderr.write(stderr);
|
|
121
|
+
const stdoutString = stdout.toString("utf8");
|
|
122
|
+
const stderrString = stderr.toString("utf8");
|
|
95
123
|
// When the cp exited, we should clean cp and the buffer to prevent memory leak.
|
|
96
124
|
stdout = Buffer.alloc(0);
|
|
97
125
|
stderr = Buffer.alloc(0);
|
|
98
126
|
cp = undefined;
|
|
99
|
-
resolve(code ?? (signal ? 128 + os.constants.signals[signal] : 1));
|
|
100
|
-
});
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
process.removeListener("SIGINT", listener);
|
|
129
|
+
process.removeListener("SIGTERM", listener);
|
|
130
|
+
resolve({
|
|
131
|
+
code: code ?? (signal ? 128 + os.constants.signals[signal] : 1),
|
|
132
|
+
stdout: stdoutString.trim(),
|
|
133
|
+
stderr: stderrString.trim(),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
108
136
|
});
|
|
109
137
|
}
|
|
110
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} topic
|
|
141
|
+
*/
|
|
142
|
+
export const execAsync = (topic) => spin(execCmd, topic);
|
|
143
|
+
|
|
111
144
|
/**
|
|
112
145
|
* @param {string} moduleName `eslint` or `prettier` or `@commitlint/cli` or `lint-staged`
|
|
113
146
|
* @param {string} from directory path or file path
|