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 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
- pnpm add -D my-code-style
16
- npx my-code-style-init
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 脚本会自动检测项目技术栈类型(uni-app / Vue 3 / Node 基础库)、ESLint 版本和 CSS 预处理器,自动按需生成对应的配置文件。
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: false, allowTemplateLiterals: true }]`
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 与配置矩阵测试(52 项)
194
+ npm test # 运行基础 CLI 与配置矩阵测试(59 项)
150
195
  npm run test:integration:setup # 安装现代化隔离依赖运行环境
151
- npm run test:integration # 运行 Flat Config、真实 Husky 及提交链路测试(44 项)
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 兼容性回归测试(25 项)
154
- npm run test:all # 全量运行全部 121 项测试
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
- // Range spanning both 8 and 9 (e.g. "^8.57.0 || ^9.0.0")
142
- if (version.includes("||")) {
143
- const parts = version.split("||").map((s) => s.trim())
144
- const has8 = parts.some((p) => /\b8(?:\.\d+)?/.test(p))
145
- const has9 = parts.some((p) => /\b9(?:\.\d+)?/.test(p))
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
- // Explicit major 8 vs 9
153
- const isMajor8 = /\b8(?:\.|\.x|\b)/.test(version) && !/^\s*(?:>=|\^)\s*9/.test(version)
154
- const isMajor9 = /\b9(?:\.|\.x|\b)/.test(version) && !/<\s*9/.test(version)
155
-
156
- if (isMajor8 && !isMajor9) return 8
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 null
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
- const stylelintFile = fileExists(".stylelintrc.cjs")
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, "mock"],
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 `npx --no-install commitlint --edit "\${1}"\n`
543
+ return readTemplate("src/husky/commit-msg").replace(/\s*$/, "\n")
338
544
  }
339
545
 
340
546
  function huskyPreCommit() {
341
- return `npx --no-install -- lint-staged\n`
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(`不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 或 9,未修改任何文件`)
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("检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs")
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
- for (const item of [...files, ...hooks]) {
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(" pnpm add -D my-code-style")
991
+ console.log(` ${addDevCommand(pm)} my-code-style`)
768
992
  if (useFlatConfig) {
769
993
  console.log(" 2. Flat Config 需要的额外依赖:")
770
- console.log(" pnpm add -D typescript-eslint globals eslint-plugin-import-x @eslint/js")
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(" pnpm prepare")
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(" pnpm prepare")
1002
+ console.log(` ${runScriptCommand(pm, "prepare")}`)
777
1003
  console.log(" 3. 使用 czg 提交 commit:")
778
1004
  }
779
- console.log(" pnpm cz")
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
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf8")
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(` pnpm add -D my-code-style \\`)
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
- main()
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.4.0",
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
- if (PLURAL_MAP[name]) {
33
- return PLURAL_MAP[name]
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 (name.endsWith("ies") && name.length > 4) {
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 (/(?:[sxz]|[ch|sh])es$/.test(name)) {
104
+ if (ES_PLURAL.test(name)) {
39
105
  return name.slice(0, -2)
40
106
  }
41
- const nonPluralEndingInS = [
42
- "status",
43
- "canvas",
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
- if (!fs.existsSync(resolved)) {
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 = fs
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)
@@ -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
- quotes: ["error", "double", { avoidEscape: false, allowTemplateLiterals: true }],
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
- quotes: ["error", "double", { avoidEscape: false, allowTemplateLiterals: true }],
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"],
@@ -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 + eslint-config-standard (via FlatCompat)
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
- // Shared rules for all JS, TS, JSX, TSX and Vue files
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: "script",
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
  ]
@@ -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
  },
package/src/gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  node_modules/
2
2
  dist/
3
+ unpackage/
3
4
  coverage/
4
5
  *.local
5
6
  *.log