my-code-style 1.4.0 → 1.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/CHANGELOG.md +109 -0
- package/LICENSE +21 -0
- package/README.md +54 -9
- package/bin/init +305 -46
- package/package.json +7 -2
- package/src/commitlint/scopes.cjs +80 -22
- package/src/eslint/base.cjs +15 -1
- package/src/eslint/flat/_shared.mjs +4 -1
- package/src/eslint/flat/base.mjs +14 -10
- package/src/eslint/flat/vue3.mjs +4 -0
- package/src/gitignore +1 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
## 变更日志
|
|
2
|
+
|
|
3
|
+
## [1.5.0](https://github.com/myvn/codeStyle/compare/v1.4.0...v1.5.0) (2026-09-18)
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes | Bug 修复
|
|
6
|
+
|
|
7
|
+
- 修复 ESLint 版本/样式检测误判、规则冲突与 scope 生成缺陷 ([033967b](https://github.com/myvn/codeStyle/commit/033967b0efadbb705bdafe5d4dfc34dbc90578c2))
|
|
8
|
+
|
|
9
|
+
### ✨ Features | 新功能
|
|
10
|
+
|
|
11
|
+
- **init:** 补全 peer 体检并按包管理器输出命令,新增初始化链路文档 ([0396198](https://github.com/myvn/codeStyle/commit/03961983be3a6f8cda96d4047450e7595ffefb97))
|
|
12
|
+
|
|
13
|
+
## [1.4.0](https://github.com/myvn/codeStyle/compare/v1.2.0...v1.4.0) (2026-09-17)
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug Fixes | Bug 修复
|
|
16
|
+
|
|
17
|
+
- **commitlint:** guessCurrentScope 仅基于暂存区推断并补充频次投票与全场景回归 ([3f5f8ea](https://github.com/myvn/codeStyle/commit/3f5f8ea1a4526621006025ce2d86608dd53e9b2d))
|
|
18
|
+
- harden initialization and lint workflows with integration tests ([7079998](https://github.com/myvn/codeStyle/commit/707999895ac4d80d311537bd833584694c7a8510))
|
|
19
|
+
- **stylelint:** 支持混合 SCSS 与 Less 工程并在组件内嵌/独立样式中全量检查与修复 ([e5a1b7e](https://github.com/myvn/codeStyle/commit/e5a1b7e4992f40cf6b4f97fa75e3c755b57fb257))
|
|
20
|
+
|
|
21
|
+
### ✨ Features | 新功能
|
|
22
|
+
|
|
23
|
+
- **core:** upgrade cli init, relax stylelint/eslint rules and bump v1.3.1 ([285c49b](https://github.com/myvn/codeStyle/commit/285c49b1f07707f7f998308d37f718e8d0af0d19))
|
|
24
|
+
- **init:** 支持 ESM versionrc、JSX/TSX 检查、版本识别强化与初始化事务回滚 ([89e8eb6](https://github.com/myvn/codeStyle/commit/89e8eb6677db00802636201148119d1fe208abe3))
|
|
25
|
+
|
|
26
|
+
## 1.3.1 (2026-09-16)
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
- **stylelint:** 放行 SCSS `@extend` 类继承语法(`scss/at-extend-no-missing-placeholder: null`),放行空样式块(`block-no-empty: null`)
|
|
31
|
+
- **stylelint:** 内置 `ignoreFiles`,自动忽略 `**/assets/iconfont/**`、`**/public/**` 及压缩文件
|
|
32
|
+
- **eslint:** 允许 `// @ts-ignore` 注释(`@typescript-eslint/ban-ts-comment: off`)及 `{}` 类型(`@typescript-eslint/no-empty-object-type: off`)
|
|
33
|
+
- **eslint:** 放行无用表达式报错(`no-unused-expressions: off`),Flat Config 默认忽略 `**/assets/iconfont/**` 与 `**/public/**`,避免历史或第三方资源阻断提交
|
|
34
|
+
|
|
35
|
+
## 1.3.0 (2026-09-16)
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
- **init:** 智能识别工程类型(uni-app / Vue 3 / Node TS 基础库),按需分层生成配置
|
|
40
|
+
- **init:** ESLint 检测支持 peerDependencies,未配置项目默认优先采用现代 Flat Config (ESLint 9+)
|
|
41
|
+
- **init:** 无样式依赖的纯代码项目自动跳过 Stylelint 配置与任务挂载
|
|
42
|
+
- **init:** 内置 `.gitignore` 模板,项目缺失时自动生成,杜绝误暂存 `node_modules`
|
|
43
|
+
- **init:** 补齐 Stylelint 配套全量预设包及 `@commitlint/config-conventional` 到一键安装提示
|
|
44
|
+
- **init:** `lint-staged` 写入改为整块规范化覆盖,彻底消除规则重复执行
|
|
45
|
+
- **core:** 工具项目自身全面规范化,接入 ESLint 9 Flat Config 与 Husky 9 提交拦截
|
|
46
|
+
|
|
47
|
+
### Bug Fixes
|
|
48
|
+
|
|
49
|
+
- **eslint:** 修复 `eslintConfigPrettier` 顺序问题,置于配置末尾消除引号冲突
|
|
50
|
+
- **eslint:** 放行 CommonJS 配置文件(`*.config.js`、`.*rc.js`、`*.cjs`)中的 `require()` 语法
|
|
51
|
+
- **eslint:** 关闭 `indent` 与 `vue/html-indent`,缩进格式全权交由 Prettier 处理
|
|
52
|
+
- **eslint:** 关闭 `import-x/extensions` 强制扩展名规则,补齐 `**/*.vue` 文件匹配
|
|
53
|
+
- **stylelint:** 补齐宽松规则,放行现代伪类语法、选择器嵌套及层叠特异性限制
|
|
54
|
+
- **husky:** 移除已废弃的 `husky.sh` 引用,升级全面兼容 Husky 9
|
|
55
|
+
- **commitlint:** 重构 `guessCurrentScope`,提升跨平台路径及多文件暂存匹配精准度
|
|
56
|
+
- **pkg:** 全部 31 项 `peerDependencies` 补充 `peerDependenciesMeta` 为 `optional`
|
|
57
|
+
|
|
58
|
+
## 1.2.0 (2026-08-28)
|
|
59
|
+
|
|
60
|
+
### Features
|
|
61
|
+
|
|
62
|
+
- 一键安装命令带上版本号 a58ddf7
|
|
63
|
+
- 一键安装提示,输出包含 my-code-style 的完整安装命令 3916ddd
|
|
64
|
+
- 安装命令分行展示,避免终端截断 3d4ab9c
|
|
65
|
+
- 添加 --version / -v 查看版本命令 14d6605
|
|
66
|
+
- 添加 Less 专用 stylelint 配置,init 脚本自动选择 be34547
|
|
67
|
+
|
|
68
|
+
### Bug Fixes
|
|
69
|
+
|
|
70
|
+
- **eslint:** 修复 ESLint v8/v9 规则覆盖、解析器配置及脚手架生成逻辑 5b4e357
|
|
71
|
+
- peerDependencies 改用 ^ 范围,限制次版本号 b8deccd
|
|
72
|
+
- 修复 init 输出步骤号跳跃(1→3→4) 5248766
|
|
73
|
+
- 修复项目自身 ESLint 配置,添加 eslint.config.mjs 83f79a7
|
|
74
|
+
- 关闭 standard 带来的 style/quotes 冲突 0a21a6b
|
|
75
|
+
- 清理项目自身 init 脚本污染 c1b3b59
|
|
76
|
+
|
|
77
|
+
## [1.1.0] - 2026-07-29
|
|
78
|
+
|
|
79
|
+
### Breaking
|
|
80
|
+
|
|
81
|
+
- ESLint >= 9.0.0、@typescript-eslint >= 8.0.0
|
|
82
|
+
- eslint-config-prettier >= 10.1.8(修复 CVE-2025-54313)
|
|
83
|
+
- eslint-plugin-vue >= 10.0.0、husky >= 9.0.0、commitlint >= 19.0.0
|
|
84
|
+
- stylelint-config-recommended >= 16.0.0、lint-staged >= 16.0.0
|
|
85
|
+
|
|
86
|
+
### Fixes
|
|
87
|
+
|
|
88
|
+
- `eslint-config-standard` 移入 peerDependenciesMeta optional(仅支持 ESLint 8)
|
|
89
|
+
|
|
90
|
+
## [1.0.2] - 2026-07-29
|
|
91
|
+
|
|
92
|
+
### Fixes
|
|
93
|
+
|
|
94
|
+
- bin 文件名去 `.cjs` 扩展名(修复 npm 警告)
|
|
95
|
+
- gitattributes 补充 `.ps1` 和 `.cmd/.bat` CRLF 支持
|
|
96
|
+
- `style/quotes` 和 `style/semi` 关闭(与 @stylistic 兼容)
|
|
97
|
+
|
|
98
|
+
## [1.0.1] - 2026-07-28
|
|
99
|
+
|
|
100
|
+
### Fixes
|
|
101
|
+
|
|
102
|
+
- 清理 init 脚本污染,.prettierrc.cjs 改为相对路径
|
|
103
|
+
|
|
104
|
+
## [1.0.0] - 2026-07-28
|
|
105
|
+
|
|
106
|
+
### Features
|
|
107
|
+
|
|
108
|
+
- Less 专用 stylelint 配置(`src/stylelint/less.cjs`)
|
|
109
|
+
- Flat Config 支持(ESLint 9+)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 myvn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,15 +8,43 @@ Lint/Format/Git 配置工程化 npm 包。适用于 Vue 3 + TypeScript + uni-app
|
|
|
8
8
|
|
|
9
9
|
- [HTML 项目技术全景](docs/project-overview.html):下载或克隆仓库后,用浏览器直接打开,查看项目结构、配置架构、接入流程与维护注意事项;支持离线阅读和打印。
|
|
10
10
|
- [Markdown 技术文档](TECHNICAL_DOC.md)
|
|
11
|
+
- [使用说明书](docs/manual.md):使用后的优点、带来的福利与解决的问题、完整使用步骤与常见问题 FAQ
|
|
12
|
+
- [初始化链路说明](docs/init-flow.md):`pnpm add -D my-code-style` + `npx my-code-style-init` 逐步做了什么、生成哪些文件、常见坑
|
|
11
13
|
|
|
12
|
-
##
|
|
14
|
+
## 怎么使用
|
|
15
|
+
|
|
16
|
+
前置条件:Node ≥ 18、项目有合法 `package.json`、**已 `git init`**(husky 需要 git 仓库)。
|
|
13
17
|
|
|
14
18
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
# 1. 安装配置包
|
|
20
|
+
pnpm add -D my-code-style # npm: npm i -D my-code-style
|
|
21
|
+
|
|
22
|
+
# 2. 先预览,再初始化(init 会覆盖已存在的 .prettierrc.cjs / hooks 等)
|
|
23
|
+
npx my-code-style-init --dry-run
|
|
24
|
+
npx my-code-style-init # 需保留旧文件时加 --backup
|
|
25
|
+
|
|
26
|
+
# 3. 按 CLI 输出的缺失依赖清单安装 peerDependencies(会按包管理器生成命令,别漏 typescript)
|
|
27
|
+
pnpm add -D my-code-style eslint@^9.0.0 typescript@^5.0.0 prettier@^3.0.0 ... # 以 CLI 实际输出为准
|
|
28
|
+
|
|
29
|
+
# 4. 初始化 hooks 并首次检查
|
|
30
|
+
pnpm prepare # npm: npm run prepare
|
|
31
|
+
pnpm lint
|
|
17
32
|
```
|
|
18
33
|
|
|
19
|
-
init
|
|
34
|
+
init 脚本会自动检测技术栈类型(uni-app / Vue 3 / Node 基础库)、ESLint 版本、CSS 预处理器和包管理器,按需生成配置文件,并在结尾列出尚未安装的 peerDependencies 与对应安装命令。
|
|
35
|
+
|
|
36
|
+
初始化后:`lint-staged` 会在 `git commit` 时自动修复暂存文件,`commitlint` 校验提交信息,`pnpm release` 生成版本与 CHANGELOG。全过程细节见 [docs/init-flow.md](docs/init-flow.md),收益与代价评估见 [docs/manual.md](docs/manual.md)。
|
|
37
|
+
|
|
38
|
+
### 使用后的变化(速览)
|
|
39
|
+
|
|
40
|
+
| 维度 | 使用前 | 使用后 |
|
|
41
|
+
| ---------- | ---------------------------- | ------------------------------------------------- |
|
|
42
|
+
| 代码格式 | 各自编辑器配置 | 全仓库统一(4 空格 / 无分号 / 双引号 / 行宽 100) |
|
|
43
|
+
| 提交 | 靠自觉,问题留到评审 | pre-commit 自动修复,修不好的挡下提交 |
|
|
44
|
+
| 提交信息 | 自由文本 | conventional commits 强校验 + `pnpm cz` 引导 |
|
|
45
|
+
| 版本与日志 | 手工改版本、手工写 CHANGELOG | `pnpm release` 自动递增并生成 CHANGELOG |
|
|
46
|
+
| 样式代码 | 基本无人检查 | stylelint 覆盖 SCSS/Less/Vue 内嵌样式 |
|
|
47
|
+
| 规则同步 | 每个项目各抄一份 | 升级一个依赖版本,全项目生效 |
|
|
20
48
|
|
|
21
49
|
## 生成的文件
|
|
22
50
|
|
|
@@ -131,7 +159,7 @@ npx my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]
|
|
|
131
159
|
|
|
132
160
|
## 特性
|
|
133
161
|
|
|
134
|
-
- **双引号**:`quotes: ["error", "double", { avoidEscape:
|
|
162
|
+
- **双引号**:`quotes: ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }]`(`avoidEscape` 必须为 `true`,否则与 Prettier 的转义策略冲突,`--fix` 无法消除报错)
|
|
135
163
|
- **4 空格缩进**:`tabWidth: 4`(由 Prettier 全权统一格式化)
|
|
136
164
|
- **无分号**:`semi: false`(`.nvue` 文件除外)
|
|
137
165
|
- **行宽 100**:`printWidth: 100`
|
|
@@ -143,15 +171,32 @@ npx my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]
|
|
|
143
171
|
- **严格暂存区 Scope 猜测**:`guessCurrentScope()` 严格只依据 Git 暂存区推断 scope,支持多目录频次投票、重命名、物理删除、中文路径与空格路径
|
|
144
172
|
- **ESM 兼容的自动版本发布**:支持 CommonJS(`.versionrc.js`)与 ESM(`.versionrc.cjs`)工程无缝对接 standard-version,一键版本递增与 CHANGELOG 生成
|
|
145
173
|
|
|
174
|
+
## 常见问题(使用中遇到的问题)
|
|
175
|
+
|
|
176
|
+
| 现象 | 原因 | 处理 |
|
|
177
|
+
| -------------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
178
|
+
| 只装了 `my-code-style`,`pnpm lint` 报找不到 eslint | peerDependencies 全部是 optional,不会自动安装 | 按 CLI 结尾输出的清单装全,**别漏 `typescript`**(`@typescript-eslint/parser` / `typescript-eslint` 的必需 peer) |
|
|
179
|
+
| `pnpm prepare` 输出 `.git can't be found`,hooks 不生效 | husky 必须在 git 仓库内执行 | 先 `git init`、再 `pnpm prepare`;验证 `git config core.hooksPath` 输出 `.husky/_` |
|
|
180
|
+
| 安装依赖报 `ERR_PNPM_IGNORED_BUILDS`(如 `unrs-resolver`) | pnpm 10+ 默认拦截依赖的构建脚本 | 执行 `pnpm approve-builds` 后重新安装,不是初始化失败 |
|
|
181
|
+
| `eslint` 声明 `>=8.0.0` 却生成了 Flat Config | 无上界范围 npm 实际会装 9,CLI 按 9 生成 | 确定留在 8 就把范围收紧到 `<9`(如 `~8.57.0`)后重跑 init |
|
|
182
|
+
| 重复 init 后,改过的 `.prettierrc.cjs` / hooks 被覆盖 | 除 ESLint 入口与已存在的 `.gitignore` 外,目标文件一律覆盖 | 先用 `--dry-run` 看清单、用 `--backup` 留档(备份含 `package.json`,目录会自动写入 `.gitignore`) |
|
|
183
|
+
| 自定义 `lint-staged` 分组消失 | `lint-staged` 为整体替换,避免残留失效分组 | 从 `.my-code-style-backup/package.json` 恢复后手动合并 |
|
|
184
|
+
| 提交时 lint 报错、提交失败 | 修不了的错误(语法错误、非法 CSS 属性)会阻止提交 | 按提示修复;紧急情况可用 `git commit --no-verify`,但请在 **CI 用 `pnpm lint` 兜底** |
|
|
185
|
+
| 提交信息被拒(`type may not be empty` / `subject may not be empty`) | 提交信息需符合 conventional commits | 用 `pnpm cz` 交互式生成;header 上限 108 字符 |
|
|
186
|
+
| 老项目已有 `.eslintrc.*`,init 只读退出 | ESLint 版本与配置格式冲突时 CLI 拒绝写入 | 先确认/迁移版本,再重跑 init |
|
|
187
|
+
| `pnpm release` 之后如何发布 | `release` 只生成版本、CHANGELOG、提交与 tag | 另行执行 `npm publish` |
|
|
188
|
+
|
|
189
|
+
完整 FAQ(含更多场景与解释)见 [docs/manual.md](docs/manual.md#五使用中遇到的问题faq)。
|
|
190
|
+
|
|
146
191
|
## 测试套件
|
|
147
192
|
|
|
148
193
|
```bash
|
|
149
|
-
npm test # 运行基础 CLI 与配置矩阵测试(
|
|
194
|
+
npm test # 运行基础 CLI 与配置矩阵测试(59 项)
|
|
150
195
|
npm run test:integration:setup # 安装现代化隔离依赖运行环境
|
|
151
|
-
npm run test:integration # 运行 Flat Config、真实 Husky 及提交链路测试(
|
|
196
|
+
npm run test:integration # 运行 Flat Config、真实 Husky 及提交链路测试(47 项)
|
|
152
197
|
npm run test:legacy:setup # 安装 ESLint 8 隔离运行环境
|
|
153
|
-
npm run test:legacy # 运行 ESLint 8.57.0 兼容性回归测试(
|
|
154
|
-
npm run test:all # 全量运行全部
|
|
198
|
+
npm run test:legacy # 运行 ESLint 8.57.0 兼容性回归测试(27 项)
|
|
199
|
+
npm run test:all # 全量运行全部 133 项测试
|
|
155
200
|
```
|
|
156
201
|
|
|
157
202
|
## License
|
package/bin/init
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require("fs")
|
|
14
14
|
const path = require("path")
|
|
15
|
-
const { execSync } = require("child_process")
|
|
16
15
|
|
|
17
16
|
const PROJECT_ROOT = process.cwd()
|
|
18
17
|
const PACKAGE_DIR = path.resolve(__dirname, "..")
|
|
@@ -51,6 +50,24 @@ function readTemplate(relativePath) {
|
|
|
51
50
|
return fs.readFileSync(path.resolve(PACKAGE_DIR, relativePath), "utf8")
|
|
52
51
|
}
|
|
53
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Append an entry to the project .gitignore (idempotent, keeps existing lines).
|
|
55
|
+
*/
|
|
56
|
+
function ensureGitignoreEntry(entry) {
|
|
57
|
+
const gitignorePath = path.resolve(PROJECT_ROOT, ".gitignore")
|
|
58
|
+
if (!fs.existsSync(gitignorePath)) return
|
|
59
|
+
const content = fs.readFileSync(gitignorePath, "utf8")
|
|
60
|
+
const hasEntry = content.split("\n").some((line) => line.trim() === entry)
|
|
61
|
+
if (hasEntry) return
|
|
62
|
+
const separator = content.length === 0 || content.endsWith("\n") ? "" : "\n"
|
|
63
|
+
fs.writeFileSync(
|
|
64
|
+
gitignorePath,
|
|
65
|
+
`${content}${separator}\n# my-code-style 初始化备份目录\n${entry}\n`,
|
|
66
|
+
"utf8",
|
|
67
|
+
)
|
|
68
|
+
log(`已将 ${entry} 追加到 .gitignore`)
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
function getProjectPkg() {
|
|
55
72
|
const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
|
|
56
73
|
if (!fs.existsSync(pkgPath)) return null
|
|
@@ -79,6 +96,142 @@ function isEsmProject(pkg = getProjectPkg()) {
|
|
|
79
96
|
return pkg?.type === "module"
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
// --- Semver range helpers (dependency-free) ---
|
|
100
|
+
// Detection only needs to know which major versions a declared range can
|
|
101
|
+
// install. Regex guessing on the raw string misreads "<9.0.0" (8 only) and
|
|
102
|
+
// ">=8.0.0" (8 or 9), so ranges are parsed into half-open [lower, upper)
|
|
103
|
+
// intervals with the subset of npm range syntax that appears in practice.
|
|
104
|
+
|
|
105
|
+
const MAX_VERSION = [Number.POSITIVE_INFINITY, 0, 0]
|
|
106
|
+
|
|
107
|
+
function compareVersion(a, b) {
|
|
108
|
+
for (let i = 0; i < 3; i += 1) {
|
|
109
|
+
if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1
|
|
110
|
+
}
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isWildcardField(field) {
|
|
115
|
+
return field === undefined || field === "" || /^[xX*]$/.test(field)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseVersionFields(text) {
|
|
119
|
+
const [release] = String(text).trim().replace(/^v/, "").split("-")
|
|
120
|
+
const fields = release.split(".")
|
|
121
|
+
if (fields.length > 3) return null
|
|
122
|
+
const parts = [0, 1, 2].map((index) => {
|
|
123
|
+
const field = fields[index]
|
|
124
|
+
if (isWildcardField(field)) return null
|
|
125
|
+
return /^\d+$/.test(field) ? parseInt(field, 10) : NaN
|
|
126
|
+
})
|
|
127
|
+
return parts.some((part) => Number.isNaN(part)) ? null : parts
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function lowerBound(parts) {
|
|
131
|
+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function partialInterval(parts) {
|
|
135
|
+
const lower = lowerBound(parts)
|
|
136
|
+
const missing = parts.findIndex((part) => part === null)
|
|
137
|
+
if (missing === -1) return [lower, [lower[0], lower[1], lower[2] + 1]]
|
|
138
|
+
if (missing === 0) return [[0, 0, 0], MAX_VERSION]
|
|
139
|
+
const upper = [0, 0, 0]
|
|
140
|
+
for (let i = 0; i < missing; i += 1) upper[i] = parts[i]
|
|
141
|
+
upper[missing - 1] += 1
|
|
142
|
+
return [lower, upper]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function caretInterval(parts) {
|
|
146
|
+
const lower = lowerBound(parts)
|
|
147
|
+
if (parts[0] === null) return [[0, 0, 0], MAX_VERSION]
|
|
148
|
+
if (parts[0] > 0 || parts[1] === null) return [lower, [parts[0] + 1, 0, 0]]
|
|
149
|
+
if (parts[1] > 0 || parts[2] === null) return [lower, [0, parts[1] + 1, 0]]
|
|
150
|
+
return [lower, [0, 0, parts[2] + 1]]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function tildeInterval(parts) {
|
|
154
|
+
const lower = lowerBound(parts)
|
|
155
|
+
if (parts[0] === null) return [[0, 0, 0], MAX_VERSION]
|
|
156
|
+
if (parts[1] === null) return [lower, [parts[0] + 1, 0, 0]]
|
|
157
|
+
return [lower, [parts[0], parts[1] + 1, 0]]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function tokenInterval(token) {
|
|
161
|
+
const match = /^(>=|<=|>|<|\^|~|=)?\s*(.*)$/.exec(token.trim())
|
|
162
|
+
if (!match || !match[2]) return null
|
|
163
|
+
const [, operator = "", text] = match
|
|
164
|
+
if (/^[xX*]$/.test(text.trim())) return [[0, 0, 0], MAX_VERSION]
|
|
165
|
+
const parts = parseVersionFields(text)
|
|
166
|
+
if (!parts) return null
|
|
167
|
+
const lower = lowerBound(parts)
|
|
168
|
+
switch (operator) {
|
|
169
|
+
case ">=":
|
|
170
|
+
return [lower, MAX_VERSION]
|
|
171
|
+
case ">":
|
|
172
|
+
return [[lower[0], lower[1], lower[2] + 1], MAX_VERSION]
|
|
173
|
+
case "<":
|
|
174
|
+
return [[0, 0, 0], lower]
|
|
175
|
+
case "<=":
|
|
176
|
+
return [
|
|
177
|
+
[0, 0, 0],
|
|
178
|
+
[lower[0], lower[1], lower[2] + 1],
|
|
179
|
+
]
|
|
180
|
+
case "^":
|
|
181
|
+
return caretInterval(parts)
|
|
182
|
+
case "~":
|
|
183
|
+
return tildeInterval(parts)
|
|
184
|
+
default:
|
|
185
|
+
return partialInterval(parts)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function rangeIntervals(range) {
|
|
190
|
+
const intervals = []
|
|
191
|
+
for (const clause of range.split("||")) {
|
|
192
|
+
const text = clause.trim()
|
|
193
|
+
if (!text) continue
|
|
194
|
+
const hyphen = text.split(/\s+-\s+/)
|
|
195
|
+
const tokens =
|
|
196
|
+
hyphen.length === 2 ? [`>=${hyphen[0]}`, `<=${hyphen[1]}`] : text.split(/\s+/)
|
|
197
|
+
let lower = [0, 0, 0]
|
|
198
|
+
let upper = MAX_VERSION
|
|
199
|
+
let valid = true
|
|
200
|
+
for (const token of tokens) {
|
|
201
|
+
const interval = tokenInterval(token)
|
|
202
|
+
if (!interval) {
|
|
203
|
+
valid = false
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
if (compareVersion(interval[0], lower) > 0) lower = interval[0]
|
|
207
|
+
if (compareVersion(interval[1], upper) < 0) upper = interval[1]
|
|
208
|
+
}
|
|
209
|
+
if (valid && compareVersion(lower, upper) < 0) intervals.push([lower, upper])
|
|
210
|
+
}
|
|
211
|
+
return intervals
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* List every major version a declared range can install (0-12), or null when
|
|
216
|
+
* the range cannot be parsed.
|
|
217
|
+
*/
|
|
218
|
+
function majorsForRange(range) {
|
|
219
|
+
const intervals = rangeIntervals(range)
|
|
220
|
+
if (!intervals.length) return null
|
|
221
|
+
const majors = []
|
|
222
|
+
for (let major = 0; major <= 12; major += 1) {
|
|
223
|
+
const lower = [major, 0, 0]
|
|
224
|
+
const upper = [major + 1, 0, 0]
|
|
225
|
+
const intersects = intervals.some(
|
|
226
|
+
([intervalLower, intervalUpper]) =>
|
|
227
|
+
compareVersion(intervalLower, upper) < 0 &&
|
|
228
|
+
compareVersion(intervalUpper, lower) > 0,
|
|
229
|
+
)
|
|
230
|
+
if (intersects) majors.push(major)
|
|
231
|
+
}
|
|
232
|
+
return majors.length ? majors : null
|
|
233
|
+
}
|
|
234
|
+
|
|
82
235
|
/**
|
|
83
236
|
* Detect ESLint major version from package.json or installed node_modules
|
|
84
237
|
* Returns 8, 9, or null (not installed or ambiguous)
|
|
@@ -138,31 +291,22 @@ function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
|
|
|
138
291
|
return 9
|
|
139
292
|
}
|
|
140
293
|
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (has8 && has9) {
|
|
147
|
-
log("ESLint 依赖同时支持 8 与 9 且未安装本地副本,默认采用 ESLint 9 Flat Config")
|
|
148
|
-
return 9
|
|
149
|
-
}
|
|
294
|
+
const majors = majorsForRange(version)
|
|
295
|
+
if (!majors) {
|
|
296
|
+
// Fallback: check leading digit if available
|
|
297
|
+
const match = version.match(/(\d+)/)
|
|
298
|
+
return match ? parseInt(match[1], 10) : null
|
|
150
299
|
}
|
|
151
300
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (isMajor9 && !isMajor8) return 9
|
|
158
|
-
|
|
159
|
-
// Fallback: check leading digit if available
|
|
160
|
-
const match = version.match(/(\d+)/)
|
|
161
|
-
if (match) {
|
|
162
|
-
return parseInt(match[1], 10)
|
|
301
|
+
// Ranges such as ">=8.0.0" or "^8.57.0 || ^9.0.0" can install either
|
|
302
|
+
// major; without a local install npm resolves the newest one (9).
|
|
303
|
+
if (majors.includes(8) && majors.includes(9)) {
|
|
304
|
+
log("ESLint 依赖同时支持 8 与 9 且未安装本地副本,默认采用 ESLint 9 Flat Config")
|
|
305
|
+
return 9
|
|
163
306
|
}
|
|
164
|
-
|
|
165
|
-
return
|
|
307
|
+
if (majors.includes(9)) return 9
|
|
308
|
+
if (majors.includes(8)) return 8
|
|
309
|
+
return majors[0]
|
|
166
310
|
}
|
|
167
311
|
|
|
168
312
|
/**
|
|
@@ -193,6 +337,52 @@ function isVueProject() {
|
|
|
193
337
|
return !!(allDeps["vue"] || allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
|
|
194
338
|
}
|
|
195
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Detect the package manager in use ("pnpm" | "npm" | "yarn" | "bun" | null).
|
|
342
|
+
* The lockfile is the project's own truth, so it wins over the user agent of
|
|
343
|
+
* whatever launched this CLI (e.g. `npx` inside a pnpm project).
|
|
344
|
+
*/
|
|
345
|
+
function detectPackageManager() {
|
|
346
|
+
if (fileExists("pnpm-lock.yaml")) return "pnpm"
|
|
347
|
+
if (fileExists("yarn.lock")) return "yarn"
|
|
348
|
+
if (fileExists("bun.lockb") || fileExists("bun.lock")) return "bun"
|
|
349
|
+
if (fileExists("package-lock.json")) return "npm"
|
|
350
|
+
|
|
351
|
+
const agent = process.env.npm_config_user_agent || ""
|
|
352
|
+
if (agent.startsWith("pnpm/")) return "pnpm"
|
|
353
|
+
if (agent.startsWith("yarn/")) return "yarn"
|
|
354
|
+
if (agent.startsWith("bun/")) return "bun"
|
|
355
|
+
if (agent.startsWith("npm/")) return "npm"
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function addDevCommand(packageManager) {
|
|
360
|
+
switch (packageManager) {
|
|
361
|
+
case "npm":
|
|
362
|
+
return "npm i -D"
|
|
363
|
+
case "yarn":
|
|
364
|
+
return "yarn add -D"
|
|
365
|
+
case "bun":
|
|
366
|
+
return "bun add -d"
|
|
367
|
+
default:
|
|
368
|
+
return "pnpm add -D"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* `pnpm prepare` / `yarn prepare` run the script directly, but npm needs an
|
|
374
|
+
* explicit `npm run prepare` (plain `npm prepare` is not a command).
|
|
375
|
+
*/
|
|
376
|
+
function runScriptCommand(packageManager, script) {
|
|
377
|
+
switch (packageManager) {
|
|
378
|
+
case "npm":
|
|
379
|
+
case "bun":
|
|
380
|
+
return `${packageManager} run ${script}`
|
|
381
|
+
default:
|
|
382
|
+
return `${packageManager} ${script}`
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
196
386
|
/**
|
|
197
387
|
* Detect project type: "uniapp", "vue", or "base"
|
|
198
388
|
*/
|
|
@@ -224,17 +414,26 @@ function detectCssPreprocessor() {
|
|
|
224
414
|
|
|
225
415
|
// Fallback: check for existing stylelint config
|
|
226
416
|
if (fileExists(".stylelintrc.cjs") || fileExists(".stylelintrc.js")) {
|
|
417
|
+
const stylelintFile = fileExists(".stylelintrc.cjs")
|
|
418
|
+
? ".stylelintrc.cjs"
|
|
419
|
+
: ".stylelintrc.js"
|
|
420
|
+
let content = ""
|
|
227
421
|
try {
|
|
228
|
-
|
|
229
|
-
? ".stylelintrc.cjs"
|
|
230
|
-
: ".stylelintrc.js"
|
|
231
|
-
const content = fs.readFileSync(path.resolve(PROJECT_ROOT, stylelintFile), "utf8")
|
|
232
|
-
const hasPostcssLess = content.includes("postcss-less")
|
|
233
|
-
const hasPostcssScss =
|
|
234
|
-
content.includes("postcss-scss") || content.includes("my-code-style/stylelint")
|
|
235
|
-
if (hasPostcssLess && hasPostcssScss) return "both"
|
|
236
|
-
if (hasPostcssLess) return "less"
|
|
422
|
+
content = fs.readFileSync(path.resolve(PROJECT_ROOT, stylelintFile), "utf8")
|
|
237
423
|
} catch {}
|
|
424
|
+
// "my-code-style/stylelint" is a prefix of "my-code-style/stylelint/less",
|
|
425
|
+
// so the Less entry must be matched before the SCSS one.
|
|
426
|
+
const usesLess =
|
|
427
|
+
/my-code-style\/stylelint\/less(?:-override)?/.test(content) ||
|
|
428
|
+
content.includes("postcss-less") ||
|
|
429
|
+
content.includes("stylelint-config-recommended-less")
|
|
430
|
+
const usesScss =
|
|
431
|
+
/my-code-style\/stylelint(?![-\w/])/.test(content) ||
|
|
432
|
+
content.includes("postcss-scss") ||
|
|
433
|
+
content.includes("stylelint-config-recommended-scss")
|
|
434
|
+
if (usesLess && usesScss) return "both"
|
|
435
|
+
if (usesLess) return "less"
|
|
436
|
+
if (usesScss) return "scss"
|
|
238
437
|
return "scss"
|
|
239
438
|
}
|
|
240
439
|
|
|
@@ -306,6 +505,11 @@ module.exports = require("${importPath}")
|
|
|
306
505
|
}
|
|
307
506
|
|
|
308
507
|
function commitlintrcContent() {
|
|
508
|
+
const mockScope = ["mock", "mocks", "src/mock", "src/mocks"].some((dir) =>
|
|
509
|
+
fs.existsSync(path.resolve(PROJECT_ROOT, dir)),
|
|
510
|
+
)
|
|
511
|
+
? `, "mock"`
|
|
512
|
+
: ""
|
|
309
513
|
return `// Commitlint config — powered by my-code-style
|
|
310
514
|
const base = require("my-code-style/commitlint")
|
|
311
515
|
const { generateScopes, guessCurrentScope } = require("my-code-style/commitlint/scopes")
|
|
@@ -319,7 +523,7 @@ module.exports = {
|
|
|
319
523
|
...base.prompt,
|
|
320
524
|
customScopesAlign: !scopeComplete ? "top" : "bottom",
|
|
321
525
|
defaultScope: scopeComplete,
|
|
322
|
-
scopes: [...scopes
|
|
526
|
+
scopes: [...scopes${mockScope}],
|
|
323
527
|
allowEmptyIssuePrefixs: false,
|
|
324
528
|
allowCustomIssuePrefixs: false,
|
|
325
529
|
},
|
|
@@ -333,12 +537,14 @@ module.exports = require("my-code-style/versionrc")
|
|
|
333
537
|
`
|
|
334
538
|
}
|
|
335
539
|
|
|
540
|
+
// Hook bodies live in src/husky/* so the shipped templates cannot drift from
|
|
541
|
+
// what the CLI writes. Normalise the trailing newline.
|
|
336
542
|
function huskyCommitMsg() {
|
|
337
|
-
return
|
|
543
|
+
return readTemplate("src/husky/commit-msg").replace(/\s*$/, "\n")
|
|
338
544
|
}
|
|
339
545
|
|
|
340
546
|
function huskyPreCommit() {
|
|
341
|
-
return
|
|
547
|
+
return readTemplate("src/husky/pre-commit").replace(/\s*$/, "\n")
|
|
342
548
|
}
|
|
343
549
|
|
|
344
550
|
function editorconfigContent() {
|
|
@@ -450,6 +656,7 @@ function main() {
|
|
|
450
656
|
}
|
|
451
657
|
|
|
452
658
|
// --- Detection phase ---
|
|
659
|
+
const pm = detectPackageManager() || "pnpm"
|
|
453
660
|
const eslintVersion = detectEslintVersion()
|
|
454
661
|
const cssPreprocessor = detectCssPreprocessor()
|
|
455
662
|
const projectType = detectProjectType()
|
|
@@ -470,11 +677,19 @@ function main() {
|
|
|
470
677
|
fileExists("eslint.config.cjs")
|
|
471
678
|
|
|
472
679
|
if (eslintVersion !== null && eslintVersion < 8) {
|
|
473
|
-
console.error(
|
|
680
|
+
console.error(
|
|
681
|
+
`不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 或 9,未修改任何文件`,
|
|
682
|
+
)
|
|
474
683
|
process.exitCode = 1
|
|
475
684
|
return
|
|
476
685
|
}
|
|
477
686
|
|
|
687
|
+
if (eslintVersion !== null && eslintVersion >= 10) {
|
|
688
|
+
warn(
|
|
689
|
+
`ESLint ${eslintVersion} 尚未在 peerDependencies 声明支持(^8.57.0 || ^9.0.0),将按 Flat Config 生成`,
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
478
693
|
// Do not silently mix an installed major version with an incompatible format.
|
|
479
694
|
if (
|
|
480
695
|
(eslintVersion >= 9 && hasLegacyConfigFile && !hasFlatConfigFile) ||
|
|
@@ -562,7 +777,9 @@ function main() {
|
|
|
562
777
|
}
|
|
563
778
|
|
|
564
779
|
if (isEsm && fileExists(".versionrc.js") && !fileExists(".versionrc.cjs")) {
|
|
565
|
-
warn(
|
|
780
|
+
warn(
|
|
781
|
+
"检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs",
|
|
782
|
+
)
|
|
566
783
|
}
|
|
567
784
|
|
|
568
785
|
files.push({
|
|
@@ -632,7 +849,9 @@ function main() {
|
|
|
632
849
|
if (backup) {
|
|
633
850
|
const backupDir = path.resolve(PROJECT_ROOT, ".my-code-style-backup")
|
|
634
851
|
ensureDir(backupDir)
|
|
635
|
-
|
|
852
|
+
// package.json is rewritten (scripts + lint-staged), so it must be backed
|
|
853
|
+
// up as well — users lose custom lint-staged groups otherwise.
|
|
854
|
+
for (const item of [...files, ...hooks, { path: "package.json" }]) {
|
|
636
855
|
if (fileExists(item.path)) {
|
|
637
856
|
const srcPath = path.resolve(PROJECT_ROOT, item.path)
|
|
638
857
|
const targetPath = path.resolve(backupDir, item.path)
|
|
@@ -722,6 +941,11 @@ function main() {
|
|
|
722
941
|
"lint-staged": getLintStagedConfig(cssPreprocessor),
|
|
723
942
|
})
|
|
724
943
|
success("更新 package.json (scripts + lint-staged)")
|
|
944
|
+
|
|
945
|
+
// The backup directory is ours, not the user's source — keep it out of git
|
|
946
|
+
if (backup) {
|
|
947
|
+
ensureGitignoreEntry(".my-code-style-backup/")
|
|
948
|
+
}
|
|
725
949
|
} catch (err) {
|
|
726
950
|
console.error(`\n ✖ 初始化写入失败: ${err.message};正在回滚...`)
|
|
727
951
|
for (const [relPath, info] of snapshot.entries()) {
|
|
@@ -764,19 +988,28 @@ function main() {
|
|
|
764
988
|
console.log("")
|
|
765
989
|
console.log(" 下一步:")
|
|
766
990
|
console.log(" 1. 确保已安装 peerDependencies:")
|
|
767
|
-
console.log(
|
|
991
|
+
console.log(` ${addDevCommand(pm)} my-code-style`)
|
|
768
992
|
if (useFlatConfig) {
|
|
769
993
|
console.log(" 2. Flat Config 需要的额外依赖:")
|
|
770
|
-
console.log(
|
|
994
|
+
console.log(
|
|
995
|
+
` ${addDevCommand(pm)} typescript-eslint globals eslint-plugin-import-x @eslint/js`,
|
|
996
|
+
)
|
|
771
997
|
console.log(" 3. 初始化 husky:")
|
|
772
|
-
console.log(
|
|
998
|
+
console.log(` ${runScriptCommand(pm, "prepare")}`)
|
|
773
999
|
console.log(" 4. 使用 czg 提交 commit:")
|
|
774
1000
|
} else {
|
|
775
1001
|
console.log(" 2. 初始化 husky:")
|
|
776
|
-
console.log(
|
|
1002
|
+
console.log(` ${runScriptCommand(pm, "prepare")}`)
|
|
777
1003
|
console.log(" 3. 使用 czg 提交 commit:")
|
|
778
1004
|
}
|
|
779
|
-
console.log(
|
|
1005
|
+
console.log(` ${runScriptCommand(pm, "cz")}`)
|
|
1006
|
+
if (pm === "pnpm") {
|
|
1007
|
+
console.log("")
|
|
1008
|
+
console.log(" 提示:pnpm 10+ 默认拦截依赖的构建脚本,若安装时提示")
|
|
1009
|
+
console.log(
|
|
1010
|
+
" ERR_PNPM_IGNORED_BUILDS(如 unrs-resolver),执行 pnpm approve-builds 后重试。",
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
780
1013
|
console.log("")
|
|
781
1014
|
}
|
|
782
1015
|
|
|
@@ -787,6 +1020,20 @@ function modifyPackageJson(updates) {
|
|
|
787
1020
|
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
788
1021
|
}
|
|
789
1022
|
|
|
1023
|
+
// A generated manifest must be installable on its own: npm requires
|
|
1024
|
+
// name/version (e.g. running init in an empty directory).
|
|
1025
|
+
if (typeof pkg.name !== "string" || !pkg.name.trim()) {
|
|
1026
|
+
pkg.name =
|
|
1027
|
+
path
|
|
1028
|
+
.basename(PROJECT_ROOT)
|
|
1029
|
+
.toLowerCase()
|
|
1030
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1031
|
+
.replace(/^[._-]+|[._-]+$/g, "") || "my-project"
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof pkg.version !== "string" || !pkg.version.trim()) {
|
|
1034
|
+
pkg.version = "1.0.0"
|
|
1035
|
+
}
|
|
1036
|
+
|
|
790
1037
|
// Merge scripts: updates won't overwrite existing scripts with the same key
|
|
791
1038
|
if (updates.scripts) {
|
|
792
1039
|
pkg.scripts = { ...updates.scripts, ...(pkg.scripts || {}) }
|
|
@@ -797,7 +1044,14 @@ function modifyPackageJson(updates) {
|
|
|
797
1044
|
pkg["lint-staged"] = updates["lint-staged"]
|
|
798
1045
|
}
|
|
799
1046
|
|
|
800
|
-
|
|
1047
|
+
// Keep the project's own indentation (a 2-space repo should not be reflowed)
|
|
1048
|
+
const previous = fileExists("package.json") ? fs.readFileSync(pkgPath, "utf8") : ""
|
|
1049
|
+
const indentMatch = previous.match(/^[ \t]+(?=")/m)
|
|
1050
|
+
fs.writeFileSync(
|
|
1051
|
+
pkgPath,
|
|
1052
|
+
JSON.stringify(pkg, null, indentMatch ? indentMatch[0] : 4) + "\n",
|
|
1053
|
+
"utf8",
|
|
1054
|
+
)
|
|
801
1055
|
}
|
|
802
1056
|
|
|
803
1057
|
function checkPeerDeps({
|
|
@@ -815,6 +1069,8 @@ function checkPeerDeps({
|
|
|
815
1069
|
|
|
816
1070
|
const baseDeps = [
|
|
817
1071
|
"eslint",
|
|
1072
|
+
// Required by @typescript-eslint/parser and typescript-eslint at runtime
|
|
1073
|
+
"typescript",
|
|
818
1074
|
"prettier",
|
|
819
1075
|
...(projectType !== "base" ? ["eslint-plugin-vue"] : []),
|
|
820
1076
|
"@commitlint/cli",
|
|
@@ -904,7 +1160,7 @@ function checkPeerDeps({
|
|
|
904
1160
|
warn(`以下依赖未安装:${missing.join(", ")}`)
|
|
905
1161
|
console.log("")
|
|
906
1162
|
warn("一键安装命令:")
|
|
907
|
-
console.log(`
|
|
1163
|
+
console.log(` ${addDevCommand(detectPackageManager())} my-code-style \\`)
|
|
908
1164
|
missing.forEach((dep, i) => {
|
|
909
1165
|
const version =
|
|
910
1166
|
dep === "eslint" ? (useFlatConfig ? "^9.0.0" : "^8.57.0") : peerDeps[dep] || ""
|
|
@@ -915,4 +1171,7 @@ function checkPeerDeps({
|
|
|
915
1171
|
}
|
|
916
1172
|
}
|
|
917
1173
|
|
|
918
|
-
|
|
1174
|
+
// Only run when executed directly; requiring this file must be side-effect free.
|
|
1175
|
+
if (require.main === module) {
|
|
1176
|
+
main()
|
|
1177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-code-style",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Lint/Format/Git 配置工程化 npm 包(ESLint + Prettier + Stylelint + Commitlint + Husky + standard-version)",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "src/eslint/uniapp.cjs",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"src",
|
|
36
36
|
"bin",
|
|
37
|
-
"README.md"
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
38
39
|
],
|
|
39
40
|
"publishConfig": {
|
|
40
41
|
"access": "public",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"stylelint-config-recommended-scss": "^16.0.0",
|
|
94
95
|
"stylelint-config-recommended-vue": "^2.0.0",
|
|
95
96
|
"stylelint-prettier": "^5.0.0",
|
|
97
|
+
"typescript": "^5.0.0",
|
|
96
98
|
"typescript-eslint": "^8.0.0",
|
|
97
99
|
"vue-eslint-parser": "^10.3.0"
|
|
98
100
|
},
|
|
@@ -184,6 +186,9 @@
|
|
|
184
186
|
"stylelint-prettier": {
|
|
185
187
|
"optional": true
|
|
186
188
|
},
|
|
189
|
+
"typescript": {
|
|
190
|
+
"optional": true
|
|
191
|
+
},
|
|
187
192
|
"typescript-eslint": {
|
|
188
193
|
"optional": true
|
|
189
194
|
},
|
|
@@ -25,34 +25,88 @@ const PLURAL_MAP = {
|
|
|
25
25
|
assets: "asset",
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Plurals whose singular cannot be derived by rule: uncountables, irregulars,
|
|
29
|
+
// and the "-e" + "s" words that would otherwise lose their stem ("caches" →
|
|
30
|
+
// "cach"). Everything else is handled by the suffix rules below.
|
|
31
|
+
const EXACT_SINGULAR = {
|
|
32
|
+
news: "news",
|
|
33
|
+
series: "series",
|
|
34
|
+
movies: "movie",
|
|
35
|
+
cookies: "cookie",
|
|
36
|
+
caches: "cache",
|
|
37
|
+
chases: "chase",
|
|
38
|
+
niches: "niche",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Directory names that already read as singular (or are conventional as-is)
|
|
42
|
+
const NON_PLURAL = [
|
|
43
|
+
"status",
|
|
44
|
+
"canvas",
|
|
45
|
+
"bus",
|
|
46
|
+
"pass",
|
|
47
|
+
"process",
|
|
48
|
+
"css",
|
|
49
|
+
"less",
|
|
50
|
+
"scss",
|
|
51
|
+
"js",
|
|
52
|
+
"ts",
|
|
53
|
+
"types",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
// Singulars that legitimately end in "s"/"z" and therefore take "-es":
|
|
57
|
+
// bus → buses, status → statuses. Without this list "buses" would follow the
|
|
58
|
+
// "-se + s" reading and become "buse".
|
|
59
|
+
const SIBILANT_SINGULARS = [
|
|
60
|
+
"bus",
|
|
61
|
+
"gas",
|
|
62
|
+
"canvas",
|
|
63
|
+
"alias",
|
|
64
|
+
"atlas",
|
|
65
|
+
"bias",
|
|
66
|
+
"virus",
|
|
67
|
+
"status",
|
|
68
|
+
"process",
|
|
69
|
+
"lens",
|
|
70
|
+
"plus",
|
|
71
|
+
"focus",
|
|
72
|
+
"campus",
|
|
73
|
+
"analysis",
|
|
74
|
+
"axis",
|
|
75
|
+
"basis",
|
|
76
|
+
"crisis",
|
|
77
|
+
"thesis",
|
|
78
|
+
"quiz",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// Unambiguous "-es" plurals: the stem ends in a doubled sibilant or a digraph
|
|
82
|
+
// (classes, boxes, churches, dishes, buzzes, batches, fixes).
|
|
83
|
+
const ES_PLURAL = /(?:ss|zz|ch|sh|x)es$/i
|
|
84
|
+
// "-ses"/"-zes" are ambiguous: "cases" is case + s, "buses" is bus + es.
|
|
85
|
+
const S_OR_Z_ES_PLURAL = /[sz]es$/i
|
|
86
|
+
|
|
28
87
|
function toSingular(name) {
|
|
29
88
|
if (!name) {
|
|
30
89
|
return name
|
|
31
90
|
}
|
|
32
|
-
|
|
33
|
-
|
|
91
|
+
const lower = name.toLowerCase()
|
|
92
|
+
if (PLURAL_MAP[lower]) {
|
|
93
|
+
return PLURAL_MAP[lower]
|
|
94
|
+
}
|
|
95
|
+
if (EXACT_SINGULAR[lower]) {
|
|
96
|
+
return EXACT_SINGULAR[lower]
|
|
34
97
|
}
|
|
35
|
-
if (
|
|
98
|
+
if (NON_PLURAL.includes(lower)) {
|
|
99
|
+
return name
|
|
100
|
+
}
|
|
101
|
+
if (/ies$/i.test(name) && name.length > 4) {
|
|
36
102
|
return name.slice(0, -3) + "y"
|
|
37
103
|
}
|
|
38
|
-
if (
|
|
104
|
+
if (ES_PLURAL.test(name)) {
|
|
39
105
|
return name.slice(0, -2)
|
|
40
106
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"bus",
|
|
45
|
-
"pass",
|
|
46
|
-
"process",
|
|
47
|
-
"css",
|
|
48
|
-
"less",
|
|
49
|
-
"scss",
|
|
50
|
-
"js",
|
|
51
|
-
"ts",
|
|
52
|
-
"types",
|
|
53
|
-
]
|
|
54
|
-
if (nonPluralEndingInS.includes(name.toLowerCase())) {
|
|
55
|
-
return name
|
|
107
|
+
if (S_OR_Z_ES_PLURAL.test(name)) {
|
|
108
|
+
const esStem = name.slice(0, -2).toLowerCase()
|
|
109
|
+
return SIBILANT_SINGULARS.includes(esStem) ? name.slice(0, -2) : name.slice(0, -1)
|
|
56
110
|
}
|
|
57
111
|
if (name.endsWith("s") && !name.endsWith("ss") && name.length > 3) {
|
|
58
112
|
return name.slice(0, -1)
|
|
@@ -71,11 +125,15 @@ function generateScopes(srcDirs = "src") {
|
|
|
71
125
|
|
|
72
126
|
for (const srcDir of dirs) {
|
|
73
127
|
const resolved = path.resolve(process.cwd(), srcDir)
|
|
74
|
-
|
|
128
|
+
let entries
|
|
129
|
+
try {
|
|
130
|
+
// A missing path, a plain file, or an unreadable directory must not
|
|
131
|
+
// break the commitlint config load — just yield no scopes.
|
|
132
|
+
entries = fs.readdirSync(resolved, { withFileTypes: true })
|
|
133
|
+
} catch {
|
|
75
134
|
continue
|
|
76
135
|
}
|
|
77
|
-
const scopes =
|
|
78
|
-
.readdirSync(resolved, { withFileTypes: true })
|
|
136
|
+
const scopes = entries
|
|
79
137
|
.filter((dirent) => dirent.isDirectory())
|
|
80
138
|
.map((dirent) => toSingular(dirent.name))
|
|
81
139
|
allScopes.push(...scopes)
|
package/src/eslint/base.cjs
CHANGED
|
@@ -20,6 +20,17 @@ module.exports = {
|
|
|
20
20
|
jsx: true,
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
+
// Mirrors the ignores of src/eslint/flat/base.mjs: without them `eslint .`
|
|
24
|
+
// lints build output (dist/, coverage/) and fails on generated code.
|
|
25
|
+
ignorePatterns: [
|
|
26
|
+
"**/node_modules/**",
|
|
27
|
+
"**/dist/**",
|
|
28
|
+
"**/coverage/**",
|
|
29
|
+
"**/public/**",
|
|
30
|
+
"**/assets/iconfont/**",
|
|
31
|
+
"**/*.min.js",
|
|
32
|
+
"**/*.min.css",
|
|
33
|
+
],
|
|
23
34
|
plugins: ["@typescript-eslint", "prettier", "import"],
|
|
24
35
|
overrides: [
|
|
25
36
|
{
|
|
@@ -42,7 +53,10 @@ module.exports = {
|
|
|
42
53
|
// 格式化与风格
|
|
43
54
|
indent: "off",
|
|
44
55
|
semi: ["error", "never"],
|
|
45
|
-
|
|
56
|
+
// avoidEscape must stay true: Prettier keeps the cheaper quote when a
|
|
57
|
+
// string contains double quotes ('say "hi"'), so requiring an escape
|
|
58
|
+
// here produces an error that --fix can never resolve.
|
|
59
|
+
quotes: ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }],
|
|
46
60
|
curly: ["error", "all"],
|
|
47
61
|
"comma-dangle": ["error", "always-multiline"],
|
|
48
62
|
"object-curly-spacing": ["error", "always"],
|
|
@@ -23,7 +23,10 @@ export const commonRules = {
|
|
|
23
23
|
// 缩进由 Prettier 统一负责,关闭 ESLint 缩进避免打架
|
|
24
24
|
indent: "off",
|
|
25
25
|
semi: ["error", "never"],
|
|
26
|
-
|
|
26
|
+
// avoidEscape must stay true: Prettier keeps the cheaper quote when a
|
|
27
|
+
// string contains double quotes ('say "hi"'), so requiring an escape here
|
|
28
|
+
// produces an error that --fix can never resolve.
|
|
29
|
+
quotes: ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }],
|
|
27
30
|
curly: ["error", "all"],
|
|
28
31
|
"comma-dangle": ["error", "always-multiline"],
|
|
29
32
|
"object-curly-spacing": ["error", "always"],
|
package/src/eslint/flat/base.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export default [
|
|
|
27
27
|
...tseslint.configs.recommended,
|
|
28
28
|
|
|
29
29
|
// eslint-plugin-import-x configuration
|
|
30
|
-
// Replaces: plugin:import/recommended
|
|
30
|
+
// Replaces: plugin:import/recommended (ESLint 8 equivalent in src/eslint/base.cjs)
|
|
31
31
|
{
|
|
32
32
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue,nvue}"],
|
|
33
33
|
plugins: {
|
|
@@ -66,7 +66,16 @@ export default [
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// Prettier integration (replaces: extends: ["plugin:prettier/recommended"])
|
|
70
|
+
eslintPluginPrettierRecommended,
|
|
71
|
+
|
|
72
|
+
// eslint-config-prettier disables rules that conflict with Prettier —
|
|
73
|
+
// including `curly`, which Prettier never rewrites.
|
|
74
|
+
eslintConfigPrettier,
|
|
75
|
+
|
|
76
|
+
// Shared rules for all JS, TS, JSX, TSX and Vue files.
|
|
77
|
+
// MUST come after eslint-config-prettier: in flat config a later entry wins,
|
|
78
|
+
// so explicit rules such as `curly` would otherwise be silently switched off.
|
|
70
79
|
{
|
|
71
80
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue,nvue}"],
|
|
72
81
|
rules: {
|
|
@@ -83,19 +92,14 @@ export default [
|
|
|
83
92
|
},
|
|
84
93
|
},
|
|
85
94
|
|
|
86
|
-
// .nvue file handling (uni-app)
|
|
95
|
+
// .nvue file handling (uni-app) — mirrors the legacy config, which parses
|
|
96
|
+
// nvue script blocks as modules so `import` works.
|
|
87
97
|
{
|
|
88
98
|
files: ["**/*.nvue"],
|
|
89
99
|
languageOptions: {
|
|
90
100
|
parserOptions: {
|
|
91
|
-
sourceType: "
|
|
101
|
+
sourceType: "module",
|
|
92
102
|
},
|
|
93
103
|
},
|
|
94
104
|
},
|
|
95
|
-
|
|
96
|
-
// Prettier integration (replaces: extends: ["plugin:prettier/recommended"])
|
|
97
|
-
eslintPluginPrettierRecommended,
|
|
98
|
-
|
|
99
|
-
// eslint-config-prettier MUST be last — disables conflicting ESLint rules
|
|
100
|
-
eslintConfigPrettier,
|
|
101
105
|
]
|
package/src/eslint/flat/vue3.mjs
CHANGED
|
@@ -49,6 +49,10 @@ export default [
|
|
|
49
49
|
},
|
|
50
50
|
rules: {
|
|
51
51
|
"vue/comment-directive": "off",
|
|
52
|
+
// .nvue uses semicolons (see src/prettier/index.cjs), so the base
|
|
53
|
+
// `semi: ["error", "never"]` rule must be disabled here — mirrors
|
|
54
|
+
// the legacy config in src/eslint/vue3.cjs.
|
|
55
|
+
semi: "off",
|
|
52
56
|
// Prettier cannot infer a parser from the custom .nvue extension.
|
|
53
57
|
"prettier/prettier": ["error", { parser: "vue", semi: true }],
|
|
54
58
|
},
|