fenge 0.0.0 → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # fenge
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 27bbb4a: feat: migrate `git-validator` to fenge
8
+
9
+ IMPORTANT: We migrate all the features from `git-validator` to `fenge` now. `git-validator` will be deprecated later.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [514733a]
14
+ - Updated dependencies [67f298b]
15
+ - Updated dependencies [db5931d]
16
+ - Updated dependencies [b5b70e3]
17
+ - @fenge/tsconfig@0.1.0
18
+ - @fenge/types@0.1.0
19
+ - @fenge/prettier-config@0.1.0
20
+ - @fenge/eslint-config@0.1.0
package/README.md CHANGED
@@ -1 +1,262 @@
1
- # fenge(风格)
1
+ <div align="center">
2
+
3
+ <img height="180" src="https://raw.githubusercontent.com/zanminkian/static/main/fenge/style.svg">
4
+
5
+ # Fenge(风格)
6
+
7
+ > A CLI tool for JavaScript and TypeScript code quality.
8
+
9
+ <font size=4> 😎 = 🇹 + 💃 + 📏 </font>
10
+
11
+ [![](https://img.shields.io/npm/l/fenge.svg)](https://github.com/zanminkian/fenge/blob/main/LICENSE)
12
+ [![](https://img.shields.io/npm/v/fenge.svg)](https://www.npmjs.com/package/fenge)
13
+ [![](https://img.shields.io/npm/dm/fenge.svg)](https://www.npmjs.com/package/fenge)
14
+ [![](https://packagephobia.com/badge?p=fenge)](https://packagephobia.com/result?p=fenge)
15
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## Philosophy
22
+
23
+ <details>
24
+ <summary>简体中文</summary>
25
+
26
+ 经过多年实践,我们发现,最影响现代 JavaScript 工程的代码质量和开发体验的主要有 3 个方面:
27
+
28
+ - **类型安全**:用于提前发现类型、拼写错误,例如对象方法是否正确调用、函数参数传递的类型是否符合函数体的期望等。
29
+ - **Formatting**:用于统一格式,提升代码可读性,减少代码冲突。主要关注例如缩进、换行、单/双引号、带/不带分号等问题。
30
+ - **Linting**:用于提前发现逻辑漏洞和糟粕用法,减少 Bug,降低维护成本。其关注点可以是除了 `Formatting` 之外的任何地方,例如重复定义变量、switch 不带 break、圈复杂度等。
31
+
32
+ > Note: `类型安全` 和 `Linting` 的关注点可能存在一定的交集,例如:“函数入参数量对不上”,既可能被类型安全的工具(如 TypeScript)检测到,也可能被 Linter(如 ESLint)检测出来。
33
+
34
+ > Note:`Formatting` 和 `Linting` 的关注点,原则上不存在交集。早期 ESLint 也被用于格式化,但是近年来,`Linter` 和 `Formatter` 分开已经被社区越来越广泛采纳,例如[ESLint 废弃 Formatting Rules](https://eslint.org/blog/2023/10/deprecating-formatting-rules)、Deno 和 Biome 均把 `Linter` 和 `Formatter` 分开。
35
+ >
36
+ > 有些人会将后两者 `Formatting` 和 `Linting` 合并起来一并处理,例如 [@antfu/eslint-config](https://github.com/antfu/eslint-config)。我们强烈**不建议**这样做。首先是因为它们目的不一样,专业的事情应该交给专业的工具。其次是它们的造成心智负担不同,Review 代码时,我们往往不需要关注 Formatting 的改动,但是我们必须要仔细检查确认 Linting 的改动,因为 Formatting 的改动一般是安全的,但是 Linting 的改动可存在错误的修复。
37
+
38
+ 这 3 个方面也是更先进的运行时 [Deno](https://deno.com) 所内置的功能,[Node](https://nodejs.org) 并没有内置支持,取而代之的是社区里百花齐放的工具:TypeScript、Flow、Biome、ESLint、oxc-lint、Prettier、dprint。这些工具用在 Node 项目中存在 3 个非常影响**开发体验**的问题:
39
+
40
+ - **工具选型问题**:我应该选择哪些工具集合来优化上述 3 个问题?选择后,下一个 Node 项目又选择不同工具集怎么办?
41
+ - **工具之间冲突磨合问题**:确定使用的工具后,这些工具之间是否有冲突?代码提交时是先 format 还是先 lint?工具之间配合的最佳实践是什么?
42
+ - **工具们的复杂配置问题**:每个工具都有很复杂难懂的配置,在项目根目录(或 `package.json` 里)到处拉屎。一来不美观简洁,二来增加理解成本。每个 Node 项目可能工具统一但配置不统一,进一步导致开发体验不一致。
43
+
44
+ 为了解决上述问题,现在有非常多教程文章讲解 TypeScript + Prettier + ESLint 的配置和实践,这些文章教程能缓解一部分问题,但仍然将<u>杂乱的工具链和繁琐的配置暴露给用户</u>。这不是我们的目标,我们的目标是**提供一个统一的工具屏蔽这些复杂的实践细节,给用户带来简单一致、开箱即用的良好开发体验**。
45
+
46
+ </details>
47
+
48
+ <details>
49
+ <summary>English</summary>
50
+ Coming soon...
51
+ </details>
52
+
53
+ ## Features
54
+
55
+ Based on the philosophy outlined above, this tool offers the following features:
56
+
57
+ - 💪 **Enhanced Type Safety**: This tool provides the strictest `tsconfig` settings and type patches to bolster the type safety of TypeScript projects. It is also compatible with pure JavaScript projects.
58
+ - 💃 **Formatting**: This tool ensures code consistency across your codebase and minimizes merge conflicts by automatically formatting code. It additionally supports the sorting of imports and `package.json` files.
59
+ - 📏 **Linting**: This tool comes equipped with a comprehensive set of rules for static code analysis, which helps catch errors and prevent poor coding practices in JavaScript.
60
+ - 🪝 **Git Hooks**: After installation, committing code via Git triggers automatic formatting and linting checks. No additional package installations are required.
61
+
62
+ ## Highlights
63
+
64
+ We place a high value on `Development Experience` (DX).
65
+
66
+ - 📦 **All-In-One**: You don't need to install `prettier`, `eslint`, `lint-staged` or `husky`.
67
+ - ⚙️ **Zero Configs**: Comes with sensible default configurations for type safety, formatting, and linting, so you don't need to set up any configurations.
68
+ - 😉 **Highly Customizable**: Every thing is optional. Every thing can be customized.
69
+
70
+ ## Quick Start
71
+
72
+ To quick start, run command below to check formatting and linting style in your project.
73
+
74
+ ```sh
75
+ npx fenge
76
+ ```
77
+
78
+ ## Install
79
+
80
+ We recommend installing it as one of `devDependencies` in your project.
81
+
82
+ ```sh
83
+ npm i -D fenge
84
+ ```
85
+
86
+ ## Usages
87
+
88
+ Each of the following usages is optional. You can choose the ones that best fit your needs.
89
+
90
+ ### Type Safe
91
+
92
+ #### Config the strictest `tsconfig.json`
93
+
94
+ Config `tsconfig.json` file in your project root.
95
+
96
+ ```json
97
+ {
98
+ "extends": "fenge/tsconfig"
99
+ }
100
+ ```
101
+
102
+ Config `tsconfig.build.json` file in sub-package or project root.
103
+
104
+ ```json
105
+ {
106
+ "extends": "./tsconfig.json",
107
+ "include": ["src"],
108
+ "exclude": ["**/*.spec.ts", "**/*.test.ts"]
109
+ }
110
+ ```
111
+
112
+ Build your project by executing:
113
+
114
+ ```sh
115
+ tsc -p ./tsconfig.build.json
116
+ ```
117
+
118
+ Type-check your project by executing:
119
+
120
+ ```sh
121
+ tsc -p ./tsconfig.build.json --noEmit
122
+ ```
123
+
124
+ For more beat practices, please refer to [@fenge/tsconfig](https://www.npmjs.com/package/@fenge/tsconfig).
125
+
126
+ #### Import type patch
127
+
128
+ Add a [triple-slash-directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) `/// <reference types="fenge/types" />` at the top of the ts file that serves as the entry point for your application or package. This will make the entire project more type-safe. The built-in type patch `fenge/types` re-exports from [@fenge/types](https://www.npmjs.com/package/@fenge/types).
129
+
130
+ Application/Package Entry Point (eg: `src/main.ts` or `src/app.ts`)
131
+
132
+ ```ts
133
+ /// <reference types="fenge/types" />
134
+ import foo from "./foo";
135
+ ```
136
+
137
+ Other File (eg: `src/other-file.ts`)
138
+
139
+ <!-- prettier-ignore-start -->
140
+ ```ts
141
+ console.log(JSON.parse('{"foo":"foo"}').bar);
142
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ❌ Object is of type 'unknown'.
143
+ ```
144
+ <!-- prettier-ignore-end -->
145
+
146
+ ### Formatting & Linting
147
+
148
+ Here are some main commands to format or lint code.
149
+
150
+ ```sh
151
+ # Check project's formatting problems only
152
+ $ fenge format
153
+
154
+ # Check project's formatting problems and apply updates
155
+ $ fenge format -u
156
+
157
+ # Check project's linting problems only
158
+ $ fenge lint
159
+
160
+ # Check project's linting problems and apply updates
161
+ $ fenge lint -u
162
+
163
+ # Check both formatting and linting problems
164
+ $ fenge
165
+
166
+ # Check both formatting and linting problems and apply updates
167
+ $ fenge -u
168
+ ```
169
+
170
+ This tool does not require a configuration file. However, you can add a `fenge.config.js` file to customize formatting and linting rules. This file should export an object with two properties:
171
+
172
+ - `format`: Accept a [Prettier Config](https://prettier.io/docs/en/configuration.html).
173
+ - `lint`: Accept a [ESLint Flat Config](https://eslint.org/docs/latest/use/configure/configuration-files).
174
+
175
+ ```js
176
+ export default {
177
+ format: {
178
+ semi: false,
179
+ singleQuote: true,
180
+ },
181
+ lint: [
182
+ {
183
+ files: ["**/*.{js,cjs,mjs,jsx}", "**/*.{ts,cts,mts,tsx}"],
184
+ rules: {
185
+ "no-unused-vars": "error",
186
+ },
187
+ },
188
+ ],
189
+ };
190
+ ```
191
+
192
+ Usually, we recommend reusing the built-in configurations rather than writing them from scratch. The built-in configurations re-export from [@fenge/prettier-config](https://www.npmjs.com/package/@fenge/prettier-config) and [@fenge/eslint-config](https://www.npmjs.com/package/@fenge/eslint-config).
193
+
194
+ ```js
195
+ // @ts-check
196
+ // See https://www.npmjs.com/package/@fenge/eslint-config for eslint-config detail usage
197
+ import { Builder } from "fenge/eslint-config";
198
+ // See https://www.npmjs.com/package/@fenge/prettier-config for prettier-config detail usage
199
+ import prettierConfig from "fenge/prettier-config";
200
+
201
+ export default {
202
+ format: {
203
+ ...prettierConfig,
204
+ // add config below to override the default behavior
205
+ semi: false,
206
+ },
207
+ lint: new Builder()
208
+ .enablePackagejson({
209
+ pick: ["packagejson/top-types"], // only these rules will work for package.json files
210
+ })
211
+ .enableJavascript({
212
+ omit: ["no-var"], // these rules will not work for js files
213
+ })
214
+ .enableTypescript({
215
+ project: "tsconfig.json", // tsconfig.json path
216
+ extend: {
217
+ // apply additional rules for ts files
218
+ "@typescript-eslint/no-explicit-any": "error",
219
+ "@typescript-eslint/consistent-type-assertions": [
220
+ "error",
221
+ { assertionStyle: "never" },
222
+ ],
223
+ "@typescript-eslint/no-non-null-assertion": "error",
224
+ },
225
+ })
226
+ .toConfig(),
227
+ };
228
+ ```
229
+
230
+ You can even install and use other third-party eslint-config, like [@sxzz/eslint-config](https://www.npmjs.com/package/@sxzz/eslint-config).
231
+
232
+ ### Set up Git hooks
233
+
234
+ Executing `fenge install` in the project root will write a `pre-commit` file to the `${PROJECT_ROOT}/.git/hooks` folder. After editing `package.json -> scripts -> prepare` script and executing it once, each commit (via Git) will trigger a code style check for the committed files.
235
+
236
+ ```json
237
+ {
238
+ "scripts": {
239
+ "prepare": "fenge install"
240
+ }
241
+ }
242
+ ```
243
+
244
+ ```sh
245
+ npm run prepare
246
+ ```
247
+
248
+ ## Contributing
249
+
250
+ - Clone this repository.
251
+ - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`.
252
+ - Install dependencies using `pnpm install`.
253
+ - Run `pnpm style:update` to develop.
254
+ - Start coding and submit your PR.
255
+
256
+ ## Show your support
257
+
258
+ Give a ⭐️ if this project helped you!
259
+
260
+ ## License
261
+
262
+ MIT
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "fenge",
3
- "version": "0.0.0",
4
- "description": "The best practice about using TypeScript, ESLint and Prettier",
3
+ "version": "0.1.0",
4
+ "description": "A CLI tool for code quality",
5
5
  "keywords": [
6
+ "cli",
7
+ "quality",
6
8
  "typescript",
9
+ "format",
10
+ "lint",
7
11
  "eslint",
8
12
  "prettier",
13
+ "biome",
9
14
  "style",
10
15
  "code-style",
11
16
  "validator",
@@ -13,7 +18,8 @@
13
18
  "husky",
14
19
  "lint-staged",
15
20
  "xo",
16
- "standard"
21
+ "standard",
22
+ "modern"
17
23
  ],
18
24
  "homepage": "https://github.com/zanminkian/fenge",
19
25
  "repository": {
@@ -22,5 +28,47 @@
22
28
  },
23
29
  "license": "MIT",
24
30
  "author": "hellozmj@qq.com",
25
- "type": "module"
31
+ "type": "module",
32
+ "exports": {
33
+ "./eslint-config": "./src/re-export/eslint.config.js",
34
+ "./prettier-config": "./src/re-export/prettier.config.js",
35
+ "./types": {
36
+ "types": "./types/index.d.ts"
37
+ },
38
+ "./tsconfig": "./tsconfig/tsconfig.json",
39
+ "./tsconfig/*": "./tsconfig/*.json",
40
+ "./tsconfig/*.json": "./tsconfig/*.json"
41
+ },
42
+ "bin": "./src/bin/cli.js",
43
+ "dependencies": {
44
+ "chalk": "5.3.0",
45
+ "commander": "12.1.0",
46
+ "eslint": "8.57.0",
47
+ "lilconfig": "3.1.2",
48
+ "lint-staged": "15.2.10",
49
+ "ora": "8.1.0",
50
+ "prettier": "3.3.3",
51
+ "@fenge/eslint-config": "0.1.0",
52
+ "@fenge/prettier-config": "0.1.0",
53
+ "@fenge/tsconfig": "0.1.0",
54
+ "@fenge/types": "0.1.0",
55
+ "prettier-ignore": "0.1.3"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "22.7.5"
59
+ },
60
+ "peerDependencies": {
61
+ "typescript": "^5.6.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "typescript": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "engines": {
69
+ "node": ">=18"
70
+ },
71
+ "scripts": {
72
+ "build": "tsc --noEmit -p tsconfig.build.json"
73
+ }
26
74
  }
package/src/bin/cli.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ import process from "node:process";
4
+ import { initAction, setup } from "@fenge/tsconfig/setup";
5
+ import { Command } from "commander";
6
+ import { format } from "../command/format.js";
7
+ import { install } from "../command/install.js";
8
+ import { lint } from "../command/lint.js";
9
+ import { importJson } from "../utils.js";
10
+
11
+ const pkgJson = await importJson(import.meta.url, "../../package.json");
12
+
13
+ const program = new Command().enablePositionalOptions();
14
+
15
+ program
16
+ .name(pkgJson.name)
17
+ .version(pkgJson.version)
18
+ .description("format and then lint code")
19
+ .option(
20
+ "-f, --fix",
21
+ "automatically fix linting problems only, will not format code",
22
+ )
23
+ .option(
24
+ "-w, --write",
25
+ "automatically format code only, will not fix linting problems",
26
+ )
27
+ .option("-u, --update", "automatically format code and fix linting problems")
28
+ .option(
29
+ "-d, --dry-run",
30
+ "print what command will be executed under the hood instead of executing",
31
+ )
32
+ .argument("[paths...]", "dir or file paths to format and lint")
33
+ .action(async (paths, options) => {
34
+ let code = (await format(paths, options)) || (await lint(paths, options));
35
+ if (options.fix || options.update) {
36
+ code ||= await format(paths, options);
37
+ }
38
+ process.exit(code);
39
+ });
40
+
41
+ program
42
+ .command("lint")
43
+ .description("lint code")
44
+ .option("-f, --fix", "automatically fix linting problems")
45
+ .option("-u, --update", "alias for '--fix' option")
46
+ .option(
47
+ "-d, --dry-run",
48
+ "print what command will be executed under the hood instead of executing",
49
+ )
50
+ .argument("[paths...]", "dir or file paths to lint")
51
+ .action(async (paths, options) => process.exit(await lint(paths, options)));
52
+
53
+ program
54
+ .command("format")
55
+ .description("format code")
56
+ .option("-w, --write", "automatically format code")
57
+ .option("-u, --update", "alias for '--write' option")
58
+ .option(
59
+ "-d, --dry-run",
60
+ "print what command will be executed under the hood instead of executing",
61
+ )
62
+ .argument("[paths...]", "dir or file paths to format")
63
+ .action(async (paths, options) => process.exit(await format(paths, options)));
64
+
65
+ program
66
+ .command("install")
67
+ .description(
68
+ "write `pre-commit` hook file into `.git/hooks` folder, after that, the committed code will be formatted and linted",
69
+ )
70
+ .option("--no-format", "skip formatting code on git 'pre-commit' stage")
71
+ .option("--no-lint", "skip linting code on git 'pre-commit' stage")
72
+ .action(async (options) => await install(options));
73
+
74
+ setup(program, {
75
+ initCommand: "init-tsconfig",
76
+ diffCommand: "diff-tsconfig",
77
+ initAction: (options) =>
78
+ initAction({ ...options, ext: `${pkgJson.name}/tsconfig` }),
79
+ });
80
+
81
+ program.parse();
@@ -0,0 +1,34 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { prettierignore } from "prettier-ignore";
5
+ import { dir, execAsync, getBinPath } from "../utils.js";
6
+
7
+ /**
8
+ * @param {Array<string>} paths
9
+ * @param {{update?: boolean, write?: boolean, dryRun?: boolean}} options
10
+ */
11
+ export async function format(paths = [], options = {}) {
12
+ const { update = false, write = false, dryRun = false } = options;
13
+
14
+ const cwd = process.cwd();
15
+ const ignores = [".gitignore", ".prettierignore", prettierignore]
16
+ .map((p) => path.resolve(p))
17
+ .flatMap((p) => ["--ignore-path", p]);
18
+ return execAsync(
19
+ [
20
+ // "node",
21
+ await getBinPath("prettier"),
22
+ ...ignores,
23
+ "--log-level",
24
+ "warn",
25
+ "--config",
26
+ path.join(dir(import.meta.url), "..", "config", "prettier.config.js"),
27
+ "--ignore-unknown",
28
+ "--no-error-on-unmatched-pattern", // Not a good option name. It's for skipping formatting symlinks. https://github.com/prettier/prettier/pull/15533
29
+ ...(update || write ? ["--write"] : ["--check"]),
30
+ ...(paths.length <= 0 ? ["."] : paths).map((p) => path.resolve(cwd, p)),
31
+ ],
32
+ { topic: "💃 Checking formatting", dryRun },
33
+ );
34
+ }
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { dir, exists, getBinPath } from "../utils.js";
6
+
7
+ /**
8
+ * @param {string} file
9
+ * @param {string} content
10
+ */
11
+ async function writeGitHook(file, content) {
12
+ const gitPath = path.resolve(process.cwd(), ".git");
13
+ if (!(await exists(gitPath))) {
14
+ throw new Error(
15
+ "Directory `.git` is not existing. Please run `git init` first.",
16
+ );
17
+ }
18
+
19
+ const hooksPath = path.resolve(gitPath, "hooks");
20
+ await fs.mkdir(hooksPath, { recursive: true });
21
+
22
+ const hookFilePath = path.resolve(hooksPath, file);
23
+ await fs.writeFile(hookFilePath, content);
24
+ await fs.chmod(hookFilePath, "777");
25
+ }
26
+
27
+ /**
28
+ * @param {{noEslint: boolean, noPrettier: boolean}} options
29
+ */
30
+ async function writePreCommit({ noEslint, noPrettier }) {
31
+ let config = "lint-staged.config.js";
32
+ if (noEslint && !noPrettier) {
33
+ config = "lint-staged-without-eslint.config.js";
34
+ } else if (!noEslint && noPrettier) {
35
+ config = "lint-staged-without-prettier.config.js";
36
+ } else if (noEslint && noPrettier) {
37
+ return;
38
+ }
39
+ const content = [
40
+ "#!/bin/sh",
41
+ `${await getBinPath("lint-staged")} --config ${path.join(dir(import.meta.url), "..", "config", config)}`,
42
+ ].join("\n");
43
+
44
+ await writeGitHook("pre-commit", content);
45
+ }
46
+
47
+ /**
48
+ * @param {{lint: boolean, format: boolean}} options
49
+ */
50
+ export async function install({ lint, format }) {
51
+ if (!lint && !format) {
52
+ throw new Error(
53
+ "'--no-lint' and '--no-format' should not be used at the same time",
54
+ );
55
+ }
56
+ await writePreCommit({ noEslint: !lint, noPrettier: !format });
57
+ }
@@ -0,0 +1,26 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { dir, execAsync, getBinPath } from "../utils.js";
5
+
6
+ /**
7
+ * @param {Array<string>} paths
8
+ * @param {{update?: boolean, fix?: boolean, dryRun?: boolean}} options
9
+ */
10
+ export async function lint(paths = [], options = {}) {
11
+ const { update = false, fix = false, dryRun = false } = options;
12
+
13
+ const cwd = process.cwd();
14
+ process.env["ESLINT_USE_FLAT_CONFIG"] = "true"; // TODO remove it once upgrade to eslint 9
15
+ return execAsync(
16
+ [
17
+ // "node",
18
+ await getBinPath("eslint"),
19
+ "--config",
20
+ path.join(dir(import.meta.url), "..", "config", "eslint.config.js"),
21
+ ...(update || fix ? ["--fix"] : []),
22
+ ...(paths.length <= 0 ? ["."] : paths).map((p) => path.resolve(cwd, p)),
23
+ ],
24
+ { topic: "📏 Checking linting", dryRun },
25
+ );
26
+ }
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+ import { resolveConfig } from "../utils.js";
3
+
4
+ export default (await resolveConfig("fenge"))?.config?.lint ??
5
+ (await resolveConfig("eslint"))?.config ??
6
+ (await import("../re-export/eslint.config.js")).default;
@@ -0,0 +1,7 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+ import { dir, resolveConfig } from "../utils.js";
4
+
5
+ const cliPath = path.resolve(dir(import.meta.url), "..", "bin", "cli.js");
6
+ const defaultConfig = { "*": [`${cliPath} format -u`] };
7
+ export default (await resolveConfig("lint-staged"))?.config ?? defaultConfig;
@@ -0,0 +1,7 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+ import { dir, resolveConfig } from "../utils.js";
4
+
5
+ const cliPath = path.resolve(dir(import.meta.url), "..", "bin", "cli.js");
6
+ const defaultConfig = { "*": [`${cliPath} lint`] };
7
+ export default (await resolveConfig("lint-staged"))?.config ?? defaultConfig;
@@ -0,0 +1,9 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+ import { dir, resolveConfig } from "../utils.js";
4
+
5
+ const cliPath = path.resolve(dir(import.meta.url), "..", "bin", "cli.js");
6
+ const defaultConfig = {
7
+ "*": [`${cliPath} -w`],
8
+ };
9
+ export default (await resolveConfig("lint-staged"))?.config ?? defaultConfig;
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+ import { resolveConfig } from "../utils.js";
3
+
4
+ export default (await resolveConfig("fenge"))?.config?.format ??
5
+ (await resolveConfig("prettier"))?.config ??
6
+ (await import("../re-export/prettier.config.js")).default;
@@ -0,0 +1,2 @@
1
+ export { default } from "@fenge/eslint-config";
2
+ export * from "@fenge/eslint-config";
@@ -0,0 +1,3 @@
1
+ // @ts-check
2
+ export { default } from "@fenge/eslint-config";
3
+ export * from "@fenge/eslint-config";
@@ -0,0 +1,2 @@
1
+ export { default } from "@fenge/prettier-config";
2
+ export * from "@fenge/prettier-config";
@@ -0,0 +1,3 @@
1
+ // @ts-check
2
+ export { default } from "@fenge/prettier-config";
3
+ export * from "@fenge/prettier-config";
package/src/utils.js ADDED
@@ -0,0 +1,158 @@
1
+ // @ts-check
2
+ import { Buffer } from "node:buffer";
3
+ import childProcess from "node:child_process";
4
+ import fs from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+ import path from "node:path";
7
+ import process from "node:process";
8
+ import { fileURLToPath } from "node:url";
9
+ import chalk from "chalk";
10
+ import { lilconfig } from "lilconfig";
11
+ import ora from "ora";
12
+
13
+ /**
14
+ * @param {string} filepath
15
+ */
16
+ export async function exists(filepath) {
17
+ return await fs
18
+ .access(filepath)
19
+ .then(() => true)
20
+ .catch(() => false);
21
+ }
22
+
23
+ /**
24
+ * Get current directory of the js file
25
+ * Usage: `dir(import.meta.url)`
26
+ * @param {string} importMetaUrl
27
+ */
28
+ export function dir(importMetaUrl) {
29
+ return path.dirname(fileURLToPath(importMetaUrl));
30
+ }
31
+
32
+ /**
33
+ * @param {string} module
34
+ */
35
+ export async function resolveConfig(module) {
36
+ return await lilconfig(module, { stopDir: process.cwd() }).search(
37
+ process.cwd(),
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Usage: `importJson(import.meta.url, '../xx.json')`
43
+ * @param {string} importMetaUrl
44
+ * @param {string} jsonPath
45
+ * @returns {Promise<any>}
46
+ */
47
+ export async function importJson(importMetaUrl, jsonPath) {
48
+ return JSON.parse(
49
+ await fs.readFile(path.resolve(dir(importMetaUrl), jsonPath), "utf-8"),
50
+ );
51
+ }
52
+
53
+ /**
54
+ * @param {number} startTime
55
+ */
56
+ function getSpentTime(startTime) {
57
+ const cost = Date.now() - startTime;
58
+ if (cost < 1000) {
59
+ return `${cost}ms`;
60
+ } else if (cost < 60 * 1000) {
61
+ return `${cost / 1000}s`;
62
+ } else {
63
+ const second = Math.floor(cost / 1000);
64
+ return `${Math.floor(second / 60)}m${Math.floor(second % 60)}s`;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @param {string[]} command
70
+ * @param {{topic: string, dryRun: boolean}} options
71
+ * @returns {Promise<number>}
72
+ */
73
+ export async function execAsync(command, { topic, dryRun }) {
74
+ const [cmd, ...args] = command;
75
+ if (!cmd) {
76
+ throw new Error("cmd not found");
77
+ }
78
+ if (dryRun) {
79
+ console.log(`${chalk.green(cmd)} ${args.join(" ")}`);
80
+ return 0;
81
+ }
82
+ const startTime = Date.now();
83
+ return new Promise((resolve) => {
84
+ const spinner = ora(`${topic}...`).start();
85
+ const cp = childProcess.spawn(cmd, args, {
86
+ env: { FORCE_COLOR: "true", ...process.env },
87
+ });
88
+ let stdout = Buffer.from([]);
89
+ let stderr = Buffer.from([]);
90
+ cp.stdout.on("data", (data) => {
91
+ stdout = Buffer.concat([stdout, data]);
92
+ });
93
+ cp.stderr.on("data", (data) => {
94
+ stderr = Buffer.concat([stderr, data]);
95
+ });
96
+ // The 'close' event will always emit after 'exit' was already emitted, or 'error' if the child failed to spawn.
97
+ cp.on("close", () => {
98
+ process.stdout.write(stdout);
99
+ process.stderr.write(stderr);
100
+ });
101
+ cp.on("error", (err) => {
102
+ spinner.fail(
103
+ `${topic} got error in ${chalk.yellow(getSpentTime(startTime))}`,
104
+ );
105
+ resolve(getExitCode(err));
106
+ });
107
+ // The 'exit' event may or may not fire after an error has occurred.
108
+ cp.on("exit", (code, signal) => {
109
+ const exitCode = getExitCode({ code, signal });
110
+ if (exitCode === 0) {
111
+ spinner.succeed(
112
+ `${topic} succeeded in ${chalk.yellow(getSpentTime(startTime))}`,
113
+ );
114
+ } else {
115
+ spinner.fail(
116
+ `${topic} failed in ${chalk.yellow(getSpentTime(startTime))}`,
117
+ );
118
+ }
119
+ resolve(exitCode);
120
+ });
121
+ process.on("SIGINT", () => !cp.killed && cp.kill("SIGINT"));
122
+ process.on("SIGTERM", () => !cp.killed && cp.kill("SIGTERM"));
123
+ });
124
+ }
125
+
126
+ /**
127
+ * @param {object} error
128
+ */
129
+ function getExitCode(error) {
130
+ if ("signal" in error && error.signal === "SIGINT") {
131
+ return 2;
132
+ }
133
+ if ("signal" in error && error.signal === "SIGTERM") {
134
+ return 15;
135
+ }
136
+ if ("code" in error && typeof error.code === "number") {
137
+ return error.code;
138
+ }
139
+ return 1;
140
+ }
141
+
142
+ /**
143
+ * @param {string} moduleName `eslint` or `prettier` or `@commitlint/cli` or `lint-staged`
144
+ * @param {string} cliName
145
+ */
146
+ export async function getBinPath(moduleName, cliName = moduleName) {
147
+ const packageJsonPath = createRequire(import.meta.url).resolve(
148
+ `${moduleName}/package.json`,
149
+ );
150
+ /** @type {any} */
151
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
152
+ const modulePath = packageJsonPath.slice(0, -"/package.json".length);
153
+ const binPath =
154
+ typeof packageJson.bin === "string"
155
+ ? packageJson.bin
156
+ : packageJson.bin[cliName];
157
+ return path.resolve(modulePath, binPath);
158
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "@fenge/tsconfig/cjs.json"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "@fenge/tsconfig/esm.json"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "@fenge/tsconfig/tsconfig.json"
3
+ }
package/tsconfig.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "extends": "../../tsconfig"
2
+ "extends": "./tsconfig/tsconfig.json"
3
3
  }
@@ -0,0 +1 @@
1
+ import "@fenge/types";