my-code-style 1.8.6 → 1.9.1
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 +18 -0
- package/README.md +60 -32
- package/bin/doctor +355 -0
- package/bin/init +98 -22
- package/package.json +3 -2
- package/src/prettierignore +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## 变更日志
|
|
2
2
|
|
|
3
|
+
## [1.9.1](https://github.com/myvn/codeStyle/compare/v1.9.0...v1.9.1) (2026-09-23)
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes | Bug 修复
|
|
6
|
+
|
|
7
|
+
- **doctor:** ESM-only peer 误报未安装——版本探测改文件系统链优先 ([418aeb5](https://github.com/myvn/codeStyle/commit/418aeb5c90b097719a37aa96ee9bf140a4550b45))
|
|
8
|
+
- **init:** --merge 统计语义「保留/覆盖」对齐 + 新增 lint:style 全量样式入口 ([9ebe796](https://github.com/myvn/codeStyle/commit/9ebe796b7959052c1880af1af164247abcc16a98))
|
|
9
|
+
|
|
10
|
+
## [1.9.0](https://github.com/myvn/codeStyle/compare/v1.8.6...v1.9.0) (2026-09-23)
|
|
11
|
+
|
|
12
|
+
### ✨ Features | 新功能
|
|
13
|
+
|
|
14
|
+
- **cli:** 新增 my-code-style-doctor 独立体检命令 ([fd3ac0d](https://github.com/myvn/codeStyle/commit/fd3ac0da3ac123f1bf69e755da1de288bdf5eeb4)), closes [#6](https://github.com/myvn/codeStyle/issues/6)
|
|
15
|
+
- **init:** --merge 合并模式 + 收尾 hook 自检 ([f6f6fa7](https://github.com/myvn/codeStyle/commit/f6f6fa7f391f4371a9cd76ff8e436c7061b25335)), closes [#7](https://github.com/myvn/codeStyle/issues/7) [#8](https://github.com/myvn/codeStyle/issues/8)
|
|
16
|
+
|
|
17
|
+
### 🐛 Bug Fixes | Bug 修复
|
|
18
|
+
|
|
19
|
+
- **prettier:** 默认 prettierignore 纳入各包管理器 lockfile(BUG-034) ([ece6d03](https://github.com/myvn/codeStyle/commit/ece6d035d3d71e64e1aca541dd49efaab615da48)), closes [#9](https://github.com/myvn/codeStyle/issues/9)
|
|
20
|
+
|
|
3
21
|
## [1.8.6](https://github.com/myvn/codeStyle/compare/v1.8.5...v1.8.6) (2026-09-22)
|
|
4
22
|
|
|
5
23
|
### 🐛 Bug Fixes | Bug 修复
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ pnpm add -D my-code-style # npm: npm i -D my-code-style
|
|
|
25
25
|
npx my-code-style-init --dry-run
|
|
26
26
|
npx my-code-style-init # 需保留旧文件时加 --backup
|
|
27
27
|
|
|
28
|
-
# 3. 按 CLI 输出的缺失依赖清单安装 peerDependencies(会按包管理器生成命令,别漏 typescript
|
|
28
|
+
# 3. 按 CLI 输出的缺失依赖清单安装 peerDependencies(会按包管理器生成命令,别漏 typescript 和 stylelint-order——后者是 recess-order 7 的 peer,Vue/SCSS 项目缺了它 stylelint 直接崩)
|
|
29
29
|
pnpm add -D my-code-style eslint@^9.0.0 typescript@^5.0.0 prettier@^3.0.0 ... # 以 CLI 实际输出为准
|
|
30
30
|
|
|
31
31
|
# 4. 初始化 hooks 并首次检查
|
|
@@ -195,13 +195,40 @@ module.exports = {
|
|
|
195
195
|
## CLI 初始化参数
|
|
196
196
|
|
|
197
197
|
```bash
|
|
198
|
-
npx my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]
|
|
198
|
+
npx my-code-style-init [--dry-run] [--backup] [--merge] [--version|-v] [--help|-h]
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
- `--dry-run`:试运行模式,仅输出将执行的变更,不修改任何文件。
|
|
202
202
|
- `--backup`:在覆盖已有配置文件之前,将其备份到 `.my-code-style-backup/` 目录。
|
|
203
|
+
- `--merge`(别名 `--no-overwrite`):合并模式——已存在的配置与 hooks 一律保留,只补缺失文件;`package.json` 只新增缺失的 scripts,已有 `lint-staged` 分组原样保留。适合升级场景先合并、再人工比对。
|
|
204
|
+
- 初始化结束时**自动自检 hooks**(存在 / 非空 / 可执行位 / `lint-staged`、`commitlint` 是否可解析),只提示不阻断——把"hook 存在但不生效"消灭在当场。
|
|
203
205
|
- 具备**事务回滚保护**:若在写入过程中遭遇文件权限或磁盘异常,自动回滚所有修改,恢复工作区原始状态。
|
|
204
206
|
|
|
207
|
+
## lint 覆盖范围须知
|
|
208
|
+
|
|
209
|
+
`lint` 脚本只跑 **ESLint**;Stylelint 默认只在 pre-commit 的 lint-staged 里对**本次改动的文件**执行。CI 如果只跑 `pnpm lint`,CSS/SCSS/Vue 内嵌样式不会被检查。有 CSS 预处理器的项目,init 会额外生成全量样式检查入口:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pnpm lint:style # stylelint "**/*.{vue,nvue,html,css,scss,less}"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
把 `lint:style` 加进 CI(或并进 `lint:all`)即可补齐覆盖面。
|
|
216
|
+
|
|
217
|
+
## 项目体检:my-code-style-doctor
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npx my-code-style-doctor
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
对既有项目随时复验(init 的依赖体检只在新装/重装时打印一次):
|
|
224
|
+
|
|
225
|
+
- **peer 依赖**:按项目类型裁剪核对(与 init 同口径——ESLint 8 才查 classic import、无预处理器不查 stylelint 族),缺失/版本越界逐项报红,并输出可整段粘贴的补齐命令;
|
|
226
|
+
- **husky hooks**:存在 / 非空 / 可执行位 / 引用命令可解析 / `core.hooksPath` 指向;
|
|
227
|
+
- **lockfile**:是否被 `.gitignore` 排除(排除 = 不同机器版本组合不可复现);
|
|
228
|
+
- **配置文件**:四件套是否就位。
|
|
229
|
+
|
|
230
|
+
退出码 `0` 健康 / `1` 有问题,可直接当 CI 门禁用。
|
|
231
|
+
|
|
205
232
|
## 特性
|
|
206
233
|
|
|
207
234
|
- **双引号**:`quotes: ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }]`(`avoidEscape` 必须为 `true`,否则与 Prettier 的转义策略冲突,`--fix` 无法消除报错)
|
|
@@ -218,32 +245,33 @@ npx my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]
|
|
|
218
245
|
|
|
219
246
|
## 常见问题(使用中遇到的问题)
|
|
220
247
|
|
|
221
|
-
| 现象 | 原因
|
|
222
|
-
| -------------------------------------------------------------------- |
|
|
223
|
-
| 只装了 `my-code-style`,`pnpm lint` 报找不到 eslint | peerDependencies 全部是 optional,不会自动安装
|
|
224
|
-
| `
|
|
225
|
-
|
|
|
226
|
-
| `
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
| `
|
|
248
|
+
| 现象 | 原因 | 处理 |
|
|
249
|
+
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
250
|
+
| 只装了 `my-code-style`,`pnpm lint` 报找不到 eslint | peerDependencies 全部是 optional,不会自动安装 | 按 CLI 结尾输出的清单装全,**别漏 `typescript`**(`@typescript-eslint/parser` / `typescript-eslint` 的必需 peer) |
|
|
251
|
+
| stylelint 报 `Could not find plugin "stylelint-order"` | recess-order 7 把 stylelint-order 声明为 peer(非依赖),pnpm 10+ 严格模式 / npm / yarn 不自动装 | 显式安装 `stylelint-order`(对齐 peer 范围 `^6 \|\| ^7 \|\| ^8`;Vue/SCSS 项目必装) |
|
|
252
|
+
| `pnpm prepare` 输出 `.git can't be found`,hooks 不生效 | husky 必须在 git 仓库内执行 | 先 `git init`、再 `pnpm prepare`;验证 `git config core.hooksPath` 输出 `.husky/_` |
|
|
253
|
+
| 安装依赖报 `ERR_PNPM_IGNORED_BUILDS`(如 `unrs-resolver`) | pnpm 10+ 默认拦截依赖的构建脚本 | 执行 `pnpm approve-builds` 后重新安装,不是初始化失败 |
|
|
254
|
+
| `eslint` 声明 `>=8.0.0` 却生成了 Flat Config | 无上界范围 npm 实际会装 9,CLI 按 9 生成 | 确定留在 8 就把范围收紧到 `<9`(如 `~8.57.0`)后重跑 init |
|
|
255
|
+
| 重复 init 后,改过的 `.prettierrc.cjs` / hooks 被覆盖 | 除 ESLint 入口与已存在的 `.gitignore` 外,目标文件一律覆盖 | 先用 `--dry-run` 看清单、用 `--backup` 留档(备份含 `package.json`,目录会自动写入 `.gitignore`) |
|
|
256
|
+
| 自定义 `lint-staged` 分组消失 | `lint-staged` 为整体替换,避免残留失效分组 | 从 `.my-code-style-backup/package.json` 恢复后手动合并 |
|
|
257
|
+
| 提交时 lint 报错、提交失败 | 修不了的错误(语法错误、非法 CSS 属性)会阻止提交 | 按提示修复;紧急情况可用 `git commit --no-verify`,但请在 **CI 用 `pnpm lint` 兜底** |
|
|
258
|
+
| 提交信息被拒(`type may not be empty` / `subject may not be empty`) | 提交信息需符合 conventional commits,`类型(范围): 描述` 的**冒号后必须有空格** | 先看冒号后面有没有空格(`fix:(修复)…` 会被判成 type 与 subject 双空);用 `pnpm cz` 交互式生成;header 上限 108 字符 |
|
|
259
|
+
| `pnpm cz` 报 `No files added to staging!` | czg 由暂存区驱动:没有暂存文件就不启动(`--all` / `-a` 也不绕过) | 先 `git add -A` 再 `pnpm cz`;scope 候选与默认值也是按暂存区推断的 |
|
|
260
|
+
| 想让 `pnpm cz` 的提示换成英文/自己的措辞 | 提示语来自 `prompt.messages`,本包默认中文 | 在项目的 `.commitlintrc.cjs` 里覆盖 `prompt.messages`(标题)与 `prompt.types`(类型描述);scope 列表里的「自定义 / 不填」对应 `customScopesAlias` / `emptyScopesAlias` |
|
|
261
|
+
| 老项目已有 `.eslintrc.*`,init 只读退出 | ESLint 版本与配置格式冲突时 CLI 拒绝写入 | 先确认/迁移版本,再重跑 init |
|
|
262
|
+
| `pnpm release` 之后如何发布 | `release` 只生成版本、CHANGELOG、提交与 tag | 另行执行 `npm publish` |
|
|
263
|
+
| 装完看到一片 `unmet peer` 警告,要不要管 | npm 遇到不满足的 peer 会直接 ERESOLVE 拒装,pnpm 只打印 WARN 就装完 | 先看警告来自谁:`unmet peer … from my-code-style` 说明版本声明没跟上,先升级本包;来自别的包(如 `postcss-html@1.8.1` 却要 `^2`)按提示对齐。重跑 `init` 会主动体检(范围精确到 minor,例如 `vue-eslint-parser 10.2.0` 不满足 `^10.3.0`)并打印对齐命令 |
|
|
264
|
+
| `stylelint` 该用 16 还是 17 | 两条线都支持:`stylelint ^16.24.0 \|\| ^17.0.0` | 16 线(`stylelint-config-recommended@17`)兼容面最广;17 线要 stylelint 17 + `config-recommended@18` + `recommended-scss@17` + `stylelint-order@7/8`,且 Node ≥ 22.12。两条线都有真实运行回归(集成套件跑 16 线,Stylelint 17 套件跑 17 线) |
|
|
237
265
|
|
|
238
266
|
完整 FAQ(含更多场景与解释)见 [docs/manual.md](docs/manual.md#五使用中遇到的问题faq)。
|
|
239
267
|
|
|
240
268
|
## 测试套件
|
|
241
269
|
|
|
242
270
|
```bash
|
|
243
|
-
npm run test:all # 全量运行
|
|
271
|
+
npm run test:all # 全量运行 197 项:用例明细 + 分类统计 + 最慢文件定位
|
|
244
272
|
npm run diagnose # 换机器后先跑它:进程 / git / 文件系统 / hooks 各占多少
|
|
245
273
|
npm run sweep # 给本机找最佳文件级并发(扫 integration 套件)
|
|
246
|
-
npm test # 只跑基础 CLI 与配置矩阵(
|
|
274
|
+
npm test # 只跑基础 CLI 与配置矩阵(96 项)
|
|
247
275
|
npm run test:integration:setup # 安装现代化隔离依赖运行环境
|
|
248
276
|
npm run test:integration # Flat Config、真实 Husky 及提交链路测试(65 项)
|
|
249
277
|
npm run test:legacy:setup # 安装 ESLint 8 隔离运行环境
|
|
@@ -258,47 +286,47 @@ npm run test:stylelint17 # Stylelint 17 生态兼容性回归(8 项)
|
|
|
258
286
|
```text
|
|
259
287
|
my-code-style 测试套件
|
|
260
288
|
|
|
261
|
-
▶ [1/4] 基础 CLI 与配置矩阵 (共
|
|
289
|
+
▶ [1/4] 基础 CLI 与配置矩阵 (共 96 项,5 文件 · 3 文件并发(自动:2 核 / 内存 4G))
|
|
262
290
|
✓ 运行器 TAP 解析:统计通过与失败、SKIP 与每用例耗时 · 7ms
|
|
263
291
|
✓ cz 交互提示为中文,且可选类型与 type-enum 一一对应 · 1ms
|
|
264
292
|
✓ peer 范围覆盖我们声明支持的上游大版本(校准防回退,BUG-027) · 0ms
|
|
265
293
|
✓ 一键安装命令对含 || 的版本范围加引号,可整段粘贴执行 · 40ms
|
|
266
294
|
✓ 初始化结尾指引把 cz 用法说清楚(先 git add + 指到 README 手册) · 80ms
|
|
267
295
|
…
|
|
268
|
-
✓ 通过
|
|
296
|
+
✓ 通过 96 · 3.6s · 最慢文件 init-matrix.test.cjs 3.4s
|
|
269
297
|
|
|
270
298
|
▶ [2/4] 现代工具链与提交链 (共 65 项,24 文件 · 3 文件并发(自动:2 核 / 内存 4G))
|
|
271
299
|
✓ 完整提交链:JS/TS/Vue/nvue/CSS/less 自动修复及二次复检 · 10.4s
|
|
272
300
|
✓ 发布防呆:HEAD 已有 v* tag 时拦截(此时 release 工具会静默抬版并写出空 CHANGELOG) · 609ms
|
|
273
301
|
…
|
|
274
|
-
✓ 通过 65 ·
|
|
302
|
+
✓ 通过 65 · 39.5s · 最慢文件 commit-chain-scss.test.cjs 12.7s
|
|
275
303
|
|
|
276
304
|
▶ [3/4] ESLint 8 兼容性 (共 27 项,1 文件 · 3 文件并发(自动:2 核 / 内存 4G))
|
|
277
305
|
✓ 独立运行 ESLint 8 支持下限而非 ESLint 9 · 1ms
|
|
278
306
|
✓ ESLint 8 base:实际 CLI 加载生成配置并接受正常文件 · 1.3s
|
|
279
307
|
…
|
|
280
|
-
✓ 通过 27 ·
|
|
308
|
+
✓ 通过 27 · 16.3s
|
|
281
309
|
|
|
282
310
|
▶ [4/4] Stylelint 17 兼容性 (共 9 项,1 文件 · 3 文件并发(自动:2 核 / 内存 4G))
|
|
283
311
|
✓ 隔离环境装的是 stylelint 17(不是 16) · 2ms
|
|
284
312
|
✓ 生成的 .stylelintrc.cjs 在 stylelint 17 下可加载并放过正常 SCSS · 700ms
|
|
285
313
|
…
|
|
286
|
-
✓ 通过 9 ·
|
|
314
|
+
✓ 通过 9 · 7.4s
|
|
287
315
|
|
|
288
316
|
──────────────────────────────────────────────────────
|
|
289
317
|
套件 通过 失败 跳过 用时
|
|
290
318
|
──────────────────────────────────────────────────────
|
|
291
|
-
基础 CLI 与配置矩阵
|
|
292
|
-
现代工具链与提交链 65 0 -
|
|
293
|
-
ESLint 8 兼容性 27 0 -
|
|
294
|
-
Stylelint 17 兼容性 9 0 -
|
|
319
|
+
基础 CLI 与配置矩阵 96 0 - 3.6s
|
|
320
|
+
现代工具链与提交链 65 0 - 39.5s
|
|
321
|
+
ESLint 8 兼容性 27 0 - 16.3s
|
|
322
|
+
Stylelint 17 兼容性 9 0 - 7.4s
|
|
295
323
|
──────────────────────────────────────────────────────
|
|
296
|
-
合计
|
|
324
|
+
合计 197 0 - 66.8s
|
|
297
325
|
──────────────────────────────────────────────────────
|
|
298
326
|
|
|
299
327
|
⏱ 最慢文件:eslint8.test.cjs 18.0s · commit-chain-less.test.cjs 11.4s · commit-chain-both.test.cjs 11.3s
|
|
300
328
|
|
|
301
|
-
✅ 全部通过:
|
|
329
|
+
✅ 全部通过:197/197 项,用时 66.8s
|
|
302
330
|
```
|
|
303
331
|
|
|
304
332
|
终端里每个套件下方还有一条实时进度条(`██████░░░░ 38/66 失败 0 4.0s`)。附加参数:
|
|
@@ -376,7 +404,7 @@ npm run test:all -- --profile # 看每个测试文件耗时,定位瓶颈
|
|
|
376
404
|
| `npm run test:integration`(运行器调度,自动并发 3) | 30.6s(只看集成套件那一段) |
|
|
377
405
|
| 串行套件 + 文件级并发 23(全部一波) | ✗ 3.8G 内存沙箱被 OOM 压垮(19 项 SIGKILL) |
|
|
378
406
|
|
|
379
|
-
同机复测(
|
|
407
|
+
同机复测(197 项,同一台 2 核沙箱连续跑;绝对秒数随机器负载波动,相对关系稳定):默认自动并发 3 = 66.8s、`npm run test:integration` = 39.5s(本轮实跑;串行与 parallel 口径为早前基线实测,相对关系稳定)。
|
|
380
408
|
|
|
381
409
|
(2 核沙箱里并行收益被 CPU 争抢吃掉大半,`--concurrency=1` 时单个文件只要 0.7–4.2s;
|
|
382
410
|
16 核机器上集成套件那段 ≈ 最慢子文件,即"一次真实提交"的量级。)
|
package/bin/doctor
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// my-code-style-doctor — 项目体检:peer 是否装齐且版本达标、hooks 是否真的能跑、
|
|
3
|
+
// lockfile 是否被 gitignore 排除、配置文件是否就位。一次性报红,供 CI 与人工复验。
|
|
4
|
+
//
|
|
5
|
+
// 背景:peerDependencies 全部 optional(包管理器零警告),"看起来配了、实际全死"
|
|
6
|
+
// 可以长期潜伏(下游 1.2 → 1.8 升级实测 20 个 peer 一个没装、hook 命令被注释)。
|
|
7
|
+
// init 的依赖体检只在新装/重装时打印一次;doctor 让既有项目随时可查(issue #6)。
|
|
8
|
+
|
|
9
|
+
const fs = require("fs")
|
|
10
|
+
const path = require("path")
|
|
11
|
+
const { spawnSync } = require("child_process")
|
|
12
|
+
|
|
13
|
+
const PROJECT_ROOT = process.cwd()
|
|
14
|
+
let problems = 0
|
|
15
|
+
|
|
16
|
+
const ok = (msg) => console.log(` ✓ ${msg}`)
|
|
17
|
+
const bad = (msg) => {
|
|
18
|
+
problems++
|
|
19
|
+
console.log(` ✗ ${msg}`)
|
|
20
|
+
}
|
|
21
|
+
const info = (msg) => console.log(` · ${msg}`)
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2)
|
|
24
|
+
const supported = new Set(["--help", "-h", "--version", "-v"])
|
|
25
|
+
const unknown = args.filter((arg) => !supported.has(arg))
|
|
26
|
+
if (unknown.length) {
|
|
27
|
+
console.error(`未知参数: ${unknown.join(", ")};使用 --help 查看帮助`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
31
|
+
console.log("Usage: my-code-style-doctor [--version|-v] [--help|-h]")
|
|
32
|
+
console.log("在项目根目录运行:体检 peer 依赖、husky hooks、lockfile 入库与配置文件。")
|
|
33
|
+
console.log("退出码:0 = 全部健康;1 = 存在需要处理的问题(可用于 CI 门禁)。")
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
37
|
+
console.log(require("../package.json").version)
|
|
38
|
+
process.exit(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- 环境解析 ----
|
|
42
|
+
const projectRequire = (() => {
|
|
43
|
+
try {
|
|
44
|
+
return require("node:module").createRequire(path.join(PROJECT_ROOT, "package.json"))
|
|
45
|
+
} catch {
|
|
46
|
+
return require
|
|
47
|
+
}
|
|
48
|
+
})()
|
|
49
|
+
|
|
50
|
+
function readManifest() {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf8"))
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 从 startDir 逐级向上直读 node_modules/<name>/package.json(纯文件系统,不经过
|
|
59
|
+
// 模块解析)。必须走这条路:ESM-only 包(如 stylelint-config-recommended@18,
|
|
60
|
+
// 下游 1.9.0 实测 BUG-035)的 exports 常常只有 "import" 条件且不暴露
|
|
61
|
+
// "./package.json",CJS require/resolve 双双抛 ERR_PACKAGE_PATH_NOT_EXPORTED,
|
|
62
|
+
// 曾被误判为"未安装"。读文件天然兼容 ESM-only / exports 任意形态 / pnpm 符号链接。
|
|
63
|
+
function findPkgUp(startDir, name) {
|
|
64
|
+
let cur = startDir
|
|
65
|
+
while (true) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(
|
|
68
|
+
fs.readFileSync(path.join(cur, "node_modules", name, "package.json"), "utf8"),
|
|
69
|
+
)
|
|
70
|
+
} catch {}
|
|
71
|
+
const parent = path.dirname(cur)
|
|
72
|
+
if (parent === cur) return null
|
|
73
|
+
cur = parent
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 读取依赖包版本:文件系统链优先, exotic 布局回退模块解析
|
|
78
|
+
function installedVersion(projectRequire, name) {
|
|
79
|
+
// ① 项目根向上(标准安装);② 本脚本位置向上(npx 临时目录 / monorepo hoisting /
|
|
80
|
+
// pnpm 内部 .pnpm 的真实包经符号链接可达)
|
|
81
|
+
const direct = findPkgUp(PROJECT_ROOT, name) ?? findPkgUp(__dirname, name)
|
|
82
|
+
if (direct?.version) return direct.version
|
|
83
|
+
try {
|
|
84
|
+
return require(`${name}/package.json`).version
|
|
85
|
+
} catch {}
|
|
86
|
+
try {
|
|
87
|
+
const entry = projectRequire.resolve(name)
|
|
88
|
+
const dir = path.dirname(entry)
|
|
89
|
+
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 迷你 semver:覆盖本包 peer 的全部形态(^x.y.z 的 || 组合、*)
|
|
96
|
+
function satisfies(version, range) {
|
|
97
|
+
const [major, minor = 0, patch = 0] = String(version)
|
|
98
|
+
.split(".")
|
|
99
|
+
.map((n) => parseInt(n, 10))
|
|
100
|
+
if ([major, minor, patch].some(Number.isNaN)) return false
|
|
101
|
+
return range
|
|
102
|
+
.split("||")
|
|
103
|
+
.map((alt) => alt.trim())
|
|
104
|
+
.some((alt) => {
|
|
105
|
+
if (!alt || alt === "*") return true
|
|
106
|
+
const caret = alt.match(/^\^(\d+)\.(\d+)\.(\d+)$/)
|
|
107
|
+
if (!caret) return false
|
|
108
|
+
const [M, m, p] = caret.slice(1).map(Number)
|
|
109
|
+
if (M > 0) return major === M
|
|
110
|
+
if (m > 0) return major === 0 && minor === m
|
|
111
|
+
return major === 0 && minor === 0 && patch === p
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`my-code-style-doctor · 项目 ${path.basename(PROJECT_ROOT)}`)
|
|
116
|
+
|
|
117
|
+
// ---- ① package.json 与 my-code-style ----
|
|
118
|
+
const manifest = readManifest()
|
|
119
|
+
if (!manifest) {
|
|
120
|
+
bad("package.json 不存在或不是合法 JSON(在项目根目录运行)")
|
|
121
|
+
console.log(`\n体检完成:${problems} 个问题`)
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
ok("package.json 可解析")
|
|
125
|
+
|
|
126
|
+
let selfPkg = null
|
|
127
|
+
try {
|
|
128
|
+
selfPkg = projectRequire("my-code-style/package.json")
|
|
129
|
+
} catch {}
|
|
130
|
+
if (!selfPkg) {
|
|
131
|
+
bad("my-code-style 未安装(npm i -D my-code-style)")
|
|
132
|
+
console.log(`\n体检完成:${problems} 个问题`)
|
|
133
|
+
process.exit(1)
|
|
134
|
+
}
|
|
135
|
+
ok(`my-code-style ${selfPkg.version}`)
|
|
136
|
+
|
|
137
|
+
// ---- ② peer 依赖:装了吗?版本对吗? ----
|
|
138
|
+
// 与 init 的依赖体检(checkPeerDeps)同口径裁剪:classic import 仅 ESLint 8 legacy
|
|
139
|
+
// 线需要;无预处理器且非 Vue 项目不查 stylelint 族;Vue 项目才查 vue 全家。
|
|
140
|
+
// 改动需与 bin/init 两侧同步(init-matrix 回归守底)。
|
|
141
|
+
function requiredPeers(manifest) {
|
|
142
|
+
const deps = {
|
|
143
|
+
...manifest.dependencies,
|
|
144
|
+
...manifest.devDependencies,
|
|
145
|
+
...manifest.peerDependencies,
|
|
146
|
+
}
|
|
147
|
+
const eslintSpec = String(deps.eslint ?? "")
|
|
148
|
+
const eslintMajor = Number((eslintSpec.match(/(\d+)\./) || [])[1] ?? 0)
|
|
149
|
+
const hasLegacyEntry = [".eslintrc.cjs", ".eslintrc.js", ".eslintrc.json"].some((f) =>
|
|
150
|
+
fs.existsSync(path.join(PROJECT_ROOT, f)),
|
|
151
|
+
)
|
|
152
|
+
const hasFlatEntry = [
|
|
153
|
+
"eslint.config.mjs",
|
|
154
|
+
"eslint.config.js",
|
|
155
|
+
"eslint.config.ts",
|
|
156
|
+
"eslint.config.cjs",
|
|
157
|
+
].some((f) => fs.existsSync(path.join(PROJECT_ROOT, f)))
|
|
158
|
+
const useFlat = hasFlatEntry || (!hasLegacyEntry && eslintMajor >= 9)
|
|
159
|
+
const isVue = !!(deps.vue || deps["@dcloudio/uni-app"] || deps["uni-app"])
|
|
160
|
+
const cssPre =
|
|
161
|
+
deps.less && (deps.sass || isVue)
|
|
162
|
+
? "both"
|
|
163
|
+
: deps.less
|
|
164
|
+
? "less"
|
|
165
|
+
: deps.sass || isVue
|
|
166
|
+
? "scss"
|
|
167
|
+
: null
|
|
168
|
+
|
|
169
|
+
const list = [
|
|
170
|
+
"eslint",
|
|
171
|
+
"typescript",
|
|
172
|
+
"prettier",
|
|
173
|
+
...(isVue ? ["eslint-plugin-vue"] : []),
|
|
174
|
+
"@commitlint/cli",
|
|
175
|
+
"@commitlint/config-conventional",
|
|
176
|
+
"husky",
|
|
177
|
+
"lint-staged",
|
|
178
|
+
"czg",
|
|
179
|
+
"commit-and-tag-version",
|
|
180
|
+
]
|
|
181
|
+
if (useFlat) {
|
|
182
|
+
list.push(
|
|
183
|
+
"typescript-eslint",
|
|
184
|
+
"eslint-import-resolver-typescript",
|
|
185
|
+
...(isVue ? ["vue-eslint-parser"] : []),
|
|
186
|
+
"globals",
|
|
187
|
+
"eslint-plugin-import-x",
|
|
188
|
+
"@eslint/js",
|
|
189
|
+
"eslint-plugin-prettier",
|
|
190
|
+
"eslint-config-prettier",
|
|
191
|
+
)
|
|
192
|
+
} else {
|
|
193
|
+
list.push(
|
|
194
|
+
"@typescript-eslint/parser",
|
|
195
|
+
"@typescript-eslint/eslint-plugin",
|
|
196
|
+
...(isVue ? ["vue-eslint-parser"] : []),
|
|
197
|
+
"eslint-plugin-import",
|
|
198
|
+
"eslint-import-resolver-typescript",
|
|
199
|
+
"eslint-plugin-prettier",
|
|
200
|
+
"eslint-config-prettier",
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
if (cssPre === "scss" || cssPre === "both") {
|
|
204
|
+
list.push(
|
|
205
|
+
"stylelint",
|
|
206
|
+
"postcss-scss",
|
|
207
|
+
"postcss-html",
|
|
208
|
+
"stylelint-config-recommended",
|
|
209
|
+
"stylelint-config-recommended-scss",
|
|
210
|
+
"stylelint-config-recommended-vue",
|
|
211
|
+
"stylelint-config-html",
|
|
212
|
+
"stylelint-config-recess-order",
|
|
213
|
+
"stylelint-prettier",
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
if (cssPre === "less" || cssPre === "both") {
|
|
217
|
+
list.push(
|
|
218
|
+
"stylelint",
|
|
219
|
+
"postcss-less",
|
|
220
|
+
"postcss-html",
|
|
221
|
+
"stylelint-config-recommended",
|
|
222
|
+
"stylelint-config-recommended-vue",
|
|
223
|
+
"stylelint-config-html",
|
|
224
|
+
"stylelint-config-recess-order",
|
|
225
|
+
"stylelint-prettier",
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
return [...new Set(list)]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const peers = selfPkg.peerDependencies || {}
|
|
232
|
+
const scopedPeers = manifest.name === "my-code-style" ? [] : requiredPeers(manifest)
|
|
233
|
+
const declared = {
|
|
234
|
+
...manifest.dependencies,
|
|
235
|
+
...manifest.devDependencies,
|
|
236
|
+
...manifest.peerDependencies,
|
|
237
|
+
}
|
|
238
|
+
const missing = []
|
|
239
|
+
const mismatched = []
|
|
240
|
+
const peerRanges = selfPkg.peerDependencies || {}
|
|
241
|
+
for (const name of scopedPeers) {
|
|
242
|
+
const range = peerRanges[name] ?? "*"
|
|
243
|
+
const version = installedVersion(projectRequire, name)
|
|
244
|
+
if (!version) {
|
|
245
|
+
missing.push([name, range])
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
if (!satisfies(version, range)) {
|
|
249
|
+
mismatched.push([name, version, range])
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (manifest.name === "my-code-style") {
|
|
253
|
+
info("my-code-style 仓库自身:跳过 peer 体检(狗粮环境按需自装)")
|
|
254
|
+
} else if (scopedPeers.length === 0) {
|
|
255
|
+
info("未检出需要体检的 peer(项目类型判定为空)")
|
|
256
|
+
} else if (missing.length === 0 && mismatched.length === 0) {
|
|
257
|
+
ok(`peer 依赖 ${scopedPeers.length} 个全部安装且版本达标`)
|
|
258
|
+
} else {
|
|
259
|
+
for (const [name, range] of missing) bad(`peer ${name} 未安装(要求 ${range})`)
|
|
260
|
+
for (const [name, version, range] of mismatched) bad(`peer ${name}@${version} 不满足 ${range}`)
|
|
261
|
+
const spec = [...missing, ...mismatched.map(([n, , r]) => [n, r])]
|
|
262
|
+
.map(([name, range]) => (range.includes("||") ? `"${name}@${range}"` : `${name}@${range}`))
|
|
263
|
+
.join(" ")
|
|
264
|
+
info(`一键补齐(按包管理器换命令):npm i -D ${spec}`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- ③ husky hooks:存在 / 非空 / 可执行 / 命令可解析 ----
|
|
268
|
+
const hooksDir = path.join(PROJECT_ROOT, ".husky")
|
|
269
|
+
const hookNames = ["pre-commit", "commit-msg"]
|
|
270
|
+
if (!fs.existsSync(hooksDir)) {
|
|
271
|
+
bad(".husky/ 不存在:先跑 init 生成 hooks,再 npm i 触发 prepare")
|
|
272
|
+
} else {
|
|
273
|
+
const gitHooksPath = spawnSync("git", ["config", "core.hooksPath"], {
|
|
274
|
+
cwd: PROJECT_ROOT,
|
|
275
|
+
encoding: "utf8",
|
|
276
|
+
})
|
|
277
|
+
if (gitHooksPath.status !== 0) {
|
|
278
|
+
info("非 git 仓库或 core.hooksPath 未设置:git init + npm run prepare 后 hooks 生效")
|
|
279
|
+
} else if (gitHooksPath.stdout.trim() !== ".husky/_") {
|
|
280
|
+
bad(`core.hooksPath=${gitHooksPath.stdout.trim()},预期 .husky/_`)
|
|
281
|
+
} else {
|
|
282
|
+
ok("core.hooksPath 指向 .husky/_")
|
|
283
|
+
}
|
|
284
|
+
for (const name of hookNames) {
|
|
285
|
+
const hookPath = path.join(hooksDir, name)
|
|
286
|
+
if (!fs.existsSync(hookPath)) {
|
|
287
|
+
bad(`.husky/${name} 缺失`)
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
const content = fs.readFileSync(hookPath, "utf8")
|
|
291
|
+
if (!content.trim()) {
|
|
292
|
+
bad(`.husky/${name} 内容为空(曾出现注释掉命令导致门禁静默失效的案例)`)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
if ((fs.statSync(hookPath).mode & 0o111) === 0) {
|
|
296
|
+
bad(`.husky/${name} 缺少可执行位,git 会静默忽略(chmod +x 修复)`)
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
let cmdOk = true
|
|
300
|
+
for (const cmd of ["lint-staged", "commitlint"]) {
|
|
301
|
+
if (!content.includes(cmd)) continue
|
|
302
|
+
const localBin = fs.existsSync(path.join(PROJECT_ROOT, "node_modules", ".bin", cmd))
|
|
303
|
+
const installed = Boolean(declared[name]) || Boolean(declared[cmd])
|
|
304
|
+
if (!localBin && !installedVersion(projectRequire, cmd) && !installed) {
|
|
305
|
+
bad(`.husky/${name} 引用的 ${cmd} 未安装:提交时会因 npx --no-install 失败`)
|
|
306
|
+
cmdOk = false
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (cmdOk) ok(`.husky/${name} 存在、可执行、引用命令可解析`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---- ④ lockfile 入库(F3):被 gitignore 的 lockfile 让版本不可复现 ----
|
|
314
|
+
const lockfiles = ["pnpm-lock.yaml", "yarn.lock", "package-lock.json", "bun.lockb", "bun.lock"]
|
|
315
|
+
const presentLock = lockfiles.find((f) => fs.existsSync(path.join(PROJECT_ROOT, f)))
|
|
316
|
+
if (!presentLock) {
|
|
317
|
+
info("未发现 lockfile(可复现安装建议入库一份)")
|
|
318
|
+
} else {
|
|
319
|
+
const check = spawnSync("git", ["check-ignore", presentLock], {
|
|
320
|
+
cwd: PROJECT_ROOT,
|
|
321
|
+
encoding: "utf8",
|
|
322
|
+
})
|
|
323
|
+
if (check.status === 0) {
|
|
324
|
+
bad(`${presentLock} 被 .gitignore 排除:不同机器/CI 的版本组合将不可复现`)
|
|
325
|
+
} else {
|
|
326
|
+
ok(`lockfile ${presentLock} 已入库`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---- ⑤ 配置文件就位 ----
|
|
331
|
+
const flatEntry = [
|
|
332
|
+
"eslint.config.mjs",
|
|
333
|
+
"eslint.config.js",
|
|
334
|
+
"eslint.config.ts",
|
|
335
|
+
"eslint.config.cjs",
|
|
336
|
+
].find((f) => fs.existsSync(path.join(PROJECT_ROOT, f)))
|
|
337
|
+
const configs = [
|
|
338
|
+
[".prettierrc.cjs", "prettier"],
|
|
339
|
+
[".stylelintrc.cjs", "stylelint(有 CSS 预处理器的项目)"],
|
|
340
|
+
[flatEntry || null, "eslint 入口"],
|
|
341
|
+
[".commitlintrc.cjs", "commitlint"],
|
|
342
|
+
]
|
|
343
|
+
let configCount = 0
|
|
344
|
+
for (const [file, label] of configs) {
|
|
345
|
+
if (!file) continue
|
|
346
|
+
if (fs.existsSync(path.join(PROJECT_ROOT, file))) {
|
|
347
|
+
configCount++
|
|
348
|
+
} else {
|
|
349
|
+
info(`缺 ${file}(${label})——需要时用 my-code-style-init 生成`)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (configCount > 0) ok(`配置文件 ${configCount} 个就位`)
|
|
353
|
+
|
|
354
|
+
console.log(`\n体检完成:${problems === 0 ? "全部健康 ✓" : `${problems} 个问题需要处理`}`)
|
|
355
|
+
process.exit(problems === 0 ? 0 : 1)
|
package/bin/init
CHANGED
|
@@ -682,7 +682,16 @@ function getLintStagedConfig(cssPreprocessor) {
|
|
|
682
682
|
|
|
683
683
|
function main() {
|
|
684
684
|
const args = process.argv.slice(2)
|
|
685
|
-
const supported = new Set([
|
|
685
|
+
const supported = new Set([
|
|
686
|
+
"--dry-run",
|
|
687
|
+
"--backup",
|
|
688
|
+
"--merge",
|
|
689
|
+
"--no-overwrite",
|
|
690
|
+
"--version",
|
|
691
|
+
"-v",
|
|
692
|
+
"--help",
|
|
693
|
+
"-h",
|
|
694
|
+
])
|
|
686
695
|
const unknown = args.filter((arg) => !supported.has(arg))
|
|
687
696
|
if (unknown.length) {
|
|
688
697
|
console.error(`未知参数: ${unknown.join(", ")};使用 --help 查看帮助`)
|
|
@@ -690,9 +699,15 @@ function main() {
|
|
|
690
699
|
return
|
|
691
700
|
}
|
|
692
701
|
if (args.includes("--help") || args.includes("-h")) {
|
|
693
|
-
console.log(
|
|
702
|
+
console.log(
|
|
703
|
+
"Usage: my-code-style-init [--dry-run] [--backup] [--merge] [--version|-v] [--help|-h]",
|
|
704
|
+
)
|
|
694
705
|
console.log(" --backup 覆盖已有配置前将其备份到 .my-code-style-backup/ 目录")
|
|
695
706
|
console.log(" --dry-run 试运行,仅输出将执行的变更,不写入任何文件")
|
|
707
|
+
console.log(
|
|
708
|
+
" --merge 合并模式:已存在的配置一律保留,只补缺失文件(别名 --no-overwrite)",
|
|
709
|
+
)
|
|
710
|
+
console.log(" 结束时自动自检 hooks(存在 / 可执行 / 命令可解析),仅提示不阻断")
|
|
696
711
|
console.log("初始化会覆盖部分配置和 hooks;请先备份并运行 --dry-run。")
|
|
697
712
|
return
|
|
698
713
|
}
|
|
@@ -907,13 +922,17 @@ function main() {
|
|
|
907
922
|
}
|
|
908
923
|
|
|
909
924
|
const backup = args.includes("--backup")
|
|
925
|
+
const mergeMode = args.includes("--merge") || args.includes("--no-overwrite")
|
|
910
926
|
stepHeader(3, 6, "配置文件与 Git hooks")
|
|
911
927
|
let overwriteTotal = 0
|
|
912
928
|
let createTotal = 0
|
|
929
|
+
// --merge 下"已存在"的语义是保留而非覆盖,统计与文案必须一致,
|
|
930
|
+
// 否则预览先吓唬人、写入段再改口"保留",前后矛盾(下游 1.9.0 实测)
|
|
931
|
+
const verb = mergeMode ? "保留" : "覆盖"
|
|
913
932
|
for (const f of files) {
|
|
914
933
|
if (f.exists) {
|
|
915
934
|
overwriteTotal++
|
|
916
|
-
console.log(`
|
|
935
|
+
console.log(` ${verb} ${f.path}`)
|
|
917
936
|
} else {
|
|
918
937
|
createTotal++
|
|
919
938
|
console.log(` 新建 ${f.path}`)
|
|
@@ -935,7 +954,7 @@ function main() {
|
|
|
935
954
|
for (const h of hooks) {
|
|
936
955
|
if (fileExists(h.path)) {
|
|
937
956
|
overwriteTotal++
|
|
938
|
-
console.log(`
|
|
957
|
+
console.log(` ${verb} ${h.path}`)
|
|
939
958
|
} else {
|
|
940
959
|
createTotal++
|
|
941
960
|
console.log(` 新建 ${h.path}`)
|
|
@@ -943,7 +962,7 @@ function main() {
|
|
|
943
962
|
}
|
|
944
963
|
|
|
945
964
|
console.log(
|
|
946
|
-
`
|
|
965
|
+
` 小计:${verb} ${overwriteTotal} 个 · 新建 ${createTotal} 个` +
|
|
947
966
|
(backup ? " · 旧文件将备份到 .my-code-style-backup/" : "") +
|
|
948
967
|
(dryRun ? "(Dry run 预览,不写入)" : ""),
|
|
949
968
|
)
|
|
@@ -1001,7 +1020,7 @@ function main() {
|
|
|
1001
1020
|
success("Dry run 结束:未写入任何文件;去掉 --dry-run 执行真实初始化")
|
|
1002
1021
|
} else {
|
|
1003
1022
|
success(
|
|
1004
|
-
|
|
1023
|
+
`初始化完成:${verb} ${overwriteTotal} 个 · 新建 ${createTotal} 个 · package.json 已更新`,
|
|
1005
1024
|
)
|
|
1006
1025
|
}
|
|
1007
1026
|
console.log("")
|
|
@@ -1023,6 +1042,8 @@ function main() {
|
|
|
1023
1042
|
// package.json is rewritten (scripts + lint-staged), so it must be backed
|
|
1024
1043
|
// up as well — users lose custom lint-staged groups otherwise.
|
|
1025
1044
|
for (const item of [...files, ...hooks, { path: "package.json" }]) {
|
|
1045
|
+
// --merge 不覆盖已有文件,无需为其备份(package.json 例外:scripts 仍会补齐)
|
|
1046
|
+
if (mergeMode && item.path !== "package.json") continue
|
|
1026
1047
|
if (fileExists(item.path)) {
|
|
1027
1048
|
const srcPath = path.resolve(PROJECT_ROOT, item.path)
|
|
1028
1049
|
const targetPath = path.resolve(backupDir, item.path)
|
|
@@ -1034,10 +1055,10 @@ function main() {
|
|
|
1034
1055
|
// 与本次生成的 .versionrc.cjs 抢生效顺序的同名旧配置(.versionrc /
|
|
1035
1056
|
// .versionrc.json 会直接压过生成物,ESM 项目的 .versionrc.js 徒增歧义):
|
|
1036
1057
|
// 备份后移除,不留"哪份在生效"的悬案(下游升级实测 D2)
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1058
|
+
// --merge 不动用户已有配置:stale 文件保持原样(前面已给出告警)
|
|
1059
|
+
const staleVersionrcFiles = mergeMode
|
|
1060
|
+
? []
|
|
1061
|
+
: [...staleVersionrcTop, ...(staleVersionrc ? [".versionrc.js"] : [])]
|
|
1041
1062
|
for (const relPath of staleVersionrcFiles) {
|
|
1042
1063
|
if (!fileExists(relPath)) continue
|
|
1043
1064
|
const staleSrc = path.resolve(PROJECT_ROOT, relPath)
|
|
@@ -1106,6 +1127,9 @@ function main() {
|
|
|
1106
1127
|
try {
|
|
1107
1128
|
// Actually write files
|
|
1108
1129
|
for (const f of files) {
|
|
1130
|
+
if (mergeMode && snapshot.get(f.path)?.existed) {
|
|
1131
|
+
continue
|
|
1132
|
+
}
|
|
1109
1133
|
writeFile(f.path, f.content)
|
|
1110
1134
|
}
|
|
1111
1135
|
|
|
@@ -1114,23 +1138,38 @@ function main() {
|
|
|
1114
1138
|
ensureDir(huskyDir)
|
|
1115
1139
|
}
|
|
1116
1140
|
for (const h of hooks) {
|
|
1141
|
+
if (mergeMode && snapshot.get(h.path)?.existed) {
|
|
1142
|
+
continue
|
|
1143
|
+
}
|
|
1117
1144
|
writeFile(h.path, h.content)
|
|
1118
1145
|
fs.chmodSync(path.resolve(PROJECT_ROOT, h.path), 0o755)
|
|
1119
1146
|
}
|
|
1120
1147
|
|
|
1121
1148
|
// package.json scripts + lint-staged
|
|
1122
|
-
modifyPackageJson(
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1149
|
+
modifyPackageJson(
|
|
1150
|
+
{
|
|
1151
|
+
scripts: {
|
|
1152
|
+
lint: lintCommand,
|
|
1153
|
+
"lint:fix": `${lintCommand} --fix`,
|
|
1154
|
+
// CI 只跑 pnpm lint 时会漏检 CSS(下游 1.9.0 反馈):给有
|
|
1155
|
+
// 预处理器的项目一个全量 stylelint 入口;ignoreFiles 已在
|
|
1156
|
+
// 配置里排除 node_modules/dist 等,glob 全仓是安全的
|
|
1157
|
+
...(skipStylelint
|
|
1158
|
+
? {}
|
|
1159
|
+
: { "lint:style": 'stylelint "**/*.{vue,nvue,html,css,scss,less}"' }),
|
|
1160
|
+
format: "prettier --write .",
|
|
1161
|
+
prepare: "husky",
|
|
1162
|
+
release: "commit-and-tag-version",
|
|
1163
|
+
cz: "czg",
|
|
1164
|
+
},
|
|
1165
|
+
"lint-staged": getLintStagedConfig(cssPreprocessor),
|
|
1130
1166
|
},
|
|
1131
|
-
|
|
1132
|
-
|
|
1167
|
+
{ merge: mergeMode },
|
|
1168
|
+
)
|
|
1133
1169
|
printPackageJsonStep()
|
|
1170
|
+
if (!dryRun) {
|
|
1171
|
+
verifyGeneratedHooks(hooks)
|
|
1172
|
+
}
|
|
1134
1173
|
|
|
1135
1174
|
// The backup directory is ours, not the user's source — keep it out of git
|
|
1136
1175
|
if (backup) {
|
|
@@ -1177,7 +1216,43 @@ function main() {
|
|
|
1177
1216
|
printFooter()
|
|
1178
1217
|
}
|
|
1179
1218
|
|
|
1180
|
-
|
|
1219
|
+
// init 收尾 hook 自检(issue #8):存在 / 非空 / 可执行位 / 引用命令可解析。
|
|
1220
|
+
// 只报告不阻断——真实提交链路才是最终裁判,这里负责把"装了但没用"提前亮出来。
|
|
1221
|
+
function verifyGeneratedHooks(hooks) {
|
|
1222
|
+
console.log(" · hook 自检")
|
|
1223
|
+
let pkg = {}
|
|
1224
|
+
try {
|
|
1225
|
+
pkg = JSON.parse(fs.readFileSync(path.resolve(PROJECT_ROOT, "package.json"), "utf8"))
|
|
1226
|
+
} catch {}
|
|
1227
|
+
for (const h of hooks) {
|
|
1228
|
+
const abs = path.resolve(PROJECT_ROOT, h.path)
|
|
1229
|
+
if (!fileExists(abs)) {
|
|
1230
|
+
warn(`${h.path} 缺失`)
|
|
1231
|
+
continue
|
|
1232
|
+
}
|
|
1233
|
+
const content = fs.readFileSync(abs, "utf8")
|
|
1234
|
+
if (!content.trim()) {
|
|
1235
|
+
warn(`${h.path} 内容为空,git 将静默跳过`)
|
|
1236
|
+
continue
|
|
1237
|
+
}
|
|
1238
|
+
if ((fs.statSync(abs).mode & 0o111) === 0) {
|
|
1239
|
+
warn(`${h.path} 缺少可执行位,git 会静默忽略(chmod +x 修复)`)
|
|
1240
|
+
}
|
|
1241
|
+
for (const cmd of ["lint-staged", "commitlint"]) {
|
|
1242
|
+
if (!content.includes(cmd)) continue
|
|
1243
|
+
const localBin = fs.existsSync(path.join(PROJECT_ROOT, "node_modules", ".bin", cmd))
|
|
1244
|
+
const declared = Boolean(pkg.devDependencies?.[cmd] ?? pkg.dependencies?.[cmd])
|
|
1245
|
+
if (!localBin && !declared) {
|
|
1246
|
+
warn(`${h.path} 引用的 ${cmd} 未安装:安装依赖前提交会因 npx --no-install 失败`)
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (!fs.existsSync(path.join(PROJECT_ROOT, ".husky", "_"))) {
|
|
1251
|
+
console.log(" · 提示:.husky/_ 未生成,安装依赖(触发 prepare)后 hooks 才生效")
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function modifyPackageJson(updates, { merge = false } = {}) {
|
|
1181
1256
|
const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
|
|
1182
1257
|
let pkg = {}
|
|
1183
1258
|
if (fileExists("package.json")) {
|
|
@@ -1214,7 +1289,8 @@ function modifyPackageJson(updates) {
|
|
|
1214
1289
|
}
|
|
1215
1290
|
|
|
1216
1291
|
// Set lint-staged (replace existing lint-staged to avoid stale/duplicate patterns)
|
|
1217
|
-
|
|
1292
|
+
// --merge 例外:用户已有分组原样保留,只补缺失
|
|
1293
|
+
if (updates["lint-staged"] && !(merge && pkg["lint-staged"])) {
|
|
1218
1294
|
pkg["lint-staged"] = updates["lint-staged"]
|
|
1219
1295
|
}
|
|
1220
1296
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-code-style",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
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",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"url": "https://github.com/myvn/codeStyle/issues"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"my-code-style-init": "./bin/init"
|
|
16
|
+
"my-code-style-init": "./bin/init",
|
|
17
|
+
"my-code-style-doctor": "./bin/doctor"
|
|
17
18
|
},
|
|
18
19
|
"exports": {
|
|
19
20
|
".": "./src/eslint/uniapp.cjs",
|
package/src/prettierignore
CHANGED
|
@@ -14,3 +14,13 @@
|
|
|
14
14
|
*.woff2
|
|
15
15
|
*.apk
|
|
16
16
|
*.plist
|
|
17
|
+
|
|
18
|
+
# 各包管理器 lockfile:格式所有权归包管理器——pnpm 9 的单引号紧凑风格
|
|
19
|
+
# 与 prettier 的 YAML 风格互搏,lint-staged 每次提交都会改写、下次 install
|
|
20
|
+
# 又被包管理器写回(下游实测单次 diff 7664 行,BUG-034 / issue #5)
|
|
21
|
+
pnpm-lock.yaml
|
|
22
|
+
yarn.lock
|
|
23
|
+
package-lock.json
|
|
24
|
+
npm-shrinkwrap.json
|
|
25
|
+
bun.lockb
|
|
26
|
+
bun.lock
|