my-code-style 1.4.0 → 1.6.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/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,25 +96,195 @@ 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
+
82
214
  /**
83
- * Detect ESLint major version from package.json or installed node_modules
84
- * Returns 8, 9, or null (not installed or ambiguous)
215
+ * List every major version a declared range can install (0-12), or null when
216
+ * the range cannot be parsed.
85
217
  */
86
- function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
87
- if (!pkg) return null
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
+ }
88
234
 
89
- // 1. If eslint is installed in node_modules, read its actual major version directly
90
- try {
91
- const installedPkgPath = path.resolve(rootDir, "node_modules", "eslint", "package.json")
92
- if (fs.existsSync(installedPkgPath)) {
93
- const installedPkg = JSON.parse(fs.readFileSync(installedPkgPath, "utf8"))
94
- const installedMajor = parseInt(installedPkg.version?.split(".")[0], 10)
95
- if (!Number.isNaN(installedMajor)) {
96
- return installedMajor
235
+ /**
236
+ * Read the actually installed ESLint major version. In a monorepo the binary is
237
+ * usually hoisted to the workspace root, so walk up to 8 levels; a version is
238
+ * only accepted from the project root itself or from an ancestor that looks
239
+ * like a workspace root (package.json / pnpm-workspace.yaml / lerna.json).
240
+ */
241
+ function findInstalledEslintMajor(rootDir) {
242
+ let dir = rootDir
243
+ for (let depth = 0; depth < 8; depth += 1) {
244
+ // 只采纳项目根或“像工作区根”的祖先里的安装(package.json / pnpm-workspace.yaml /
245
+ // lerna.json)。中间层(如 monorepo 的 packages/)通常没有任何 manifest,
246
+ // 必须继续向上找;而完全无关的上级目录(例如 /tmp 或 $HOME 的 node_modules)
247
+ // 即使存在 eslint 也不能当作项目依赖。
248
+ const isProjectRoot = dir === rootDir
249
+ const isWorkspaceRoot =
250
+ fs.existsSync(path.resolve(dir, "package.json")) ||
251
+ fs.existsSync(path.resolve(dir, "pnpm-workspace.yaml")) ||
252
+ fs.existsSync(path.resolve(dir, "lerna.json"))
253
+
254
+ if (isProjectRoot || isWorkspaceRoot) {
255
+ try {
256
+ const installedPkgPath = path.resolve(dir, "node_modules", "eslint", "package.json")
257
+ if (fs.existsSync(installedPkgPath)) {
258
+ const installedPkg = JSON.parse(fs.readFileSync(installedPkgPath, "utf8"))
259
+ const installedMajor = parseInt(String(installedPkg.version).split(".")[0], 10)
260
+ if (!Number.isNaN(installedMajor)) {
261
+ return installedMajor
262
+ }
263
+ }
264
+ } catch {
265
+ // ignore unreadable/partial installs and keep walking up
97
266
  }
98
267
  }
99
- } catch {
100
- // Fall through to package.json dependency declaration
268
+
269
+ const parent = path.dirname(dir)
270
+ if (parent === dir) break
271
+ dir = parent
272
+ }
273
+ return null
274
+ }
275
+
276
+ /**
277
+ * Detect ESLint major version from the installed package or the declared range.
278
+ * Returns 8, 9, 10, … or null (not installed and no usable range).
279
+ */
280
+ function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
281
+ if (!pkg) return null
282
+
283
+ // 1. If eslint is installed in node_modules (or hoisted to the workspace root),
284
+ // read its actual major version directly
285
+ const installedMajor = findInstalledEslintMajor(rootDir)
286
+ if (installedMajor !== null) {
287
+ return installedMajor
101
288
  }
102
289
 
103
290
  // 2. Parse package.json dependency declaration
@@ -138,31 +325,22 @@ function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
138
325
  return 9
139
326
  }
140
327
 
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
- }
328
+ const majors = majorsForRange(version)
329
+ if (!majors) {
330
+ // Fallback: check leading digit if available
331
+ const match = version.match(/(\d+)/)
332
+ return match ? parseInt(match[1], 10) : null
150
333
  }
151
334
 
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)
335
+ // Ranges such as ">=8.0.0" or "^8.57.0 || ^9.0.0" can install either
336
+ // major; without a local install npm resolves the newest one (9).
337
+ if (majors.includes(8) && majors.includes(9)) {
338
+ log("ESLint 依赖同时支持 8 与 9 且未安装本地副本,默认采用 ESLint 9 Flat Config")
339
+ return 9
163
340
  }
164
-
165
- return null
341
+ if (majors.includes(9)) return 9
342
+ if (majors.includes(8)) return 8
343
+ return majors[0]
166
344
  }
167
345
 
168
346
  /**
@@ -193,6 +371,52 @@ function isVueProject() {
193
371
  return !!(allDeps["vue"] || allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
194
372
  }
195
373
 
374
+ /**
375
+ * Detect the package manager in use ("pnpm" | "npm" | "yarn" | "bun" | null).
376
+ * The lockfile is the project's own truth, so it wins over the user agent of
377
+ * whatever launched this CLI (e.g. `npx` inside a pnpm project).
378
+ */
379
+ function detectPackageManager() {
380
+ if (fileExists("pnpm-lock.yaml")) return "pnpm"
381
+ if (fileExists("yarn.lock")) return "yarn"
382
+ if (fileExists("bun.lockb") || fileExists("bun.lock")) return "bun"
383
+ if (fileExists("package-lock.json")) return "npm"
384
+
385
+ const agent = process.env.npm_config_user_agent || ""
386
+ if (agent.startsWith("pnpm/")) return "pnpm"
387
+ if (agent.startsWith("yarn/")) return "yarn"
388
+ if (agent.startsWith("bun/")) return "bun"
389
+ if (agent.startsWith("npm/")) return "npm"
390
+ return null
391
+ }
392
+
393
+ function addDevCommand(packageManager) {
394
+ switch (packageManager) {
395
+ case "npm":
396
+ return "npm i -D"
397
+ case "yarn":
398
+ return "yarn add -D"
399
+ case "bun":
400
+ return "bun add -d"
401
+ default:
402
+ return "pnpm add -D"
403
+ }
404
+ }
405
+
406
+ /**
407
+ * `pnpm prepare` / `yarn prepare` run the script directly, but npm needs an
408
+ * explicit `npm run prepare` (plain `npm prepare` is not a command).
409
+ */
410
+ function runScriptCommand(packageManager, script) {
411
+ switch (packageManager) {
412
+ case "npm":
413
+ case "bun":
414
+ return `${packageManager} run ${script}`
415
+ default:
416
+ return `${packageManager} ${script}`
417
+ }
418
+ }
419
+
196
420
  /**
197
421
  * Detect project type: "uniapp", "vue", or "base"
198
422
  */
@@ -208,12 +432,13 @@ function detectProjectType() {
208
432
  */
209
433
  function detectCssPreprocessor() {
210
434
  const pkg = getProjectPkg()
211
- if (!pkg) return "scss"
212
- const allDeps = {
213
- ...(pkg.dependencies || {}),
214
- ...(pkg.devDependencies || {}),
215
- ...(pkg.peerDependencies || {}),
216
- }
435
+ const allDeps = pkg
436
+ ? {
437
+ ...(pkg.dependencies || {}),
438
+ ...(pkg.devDependencies || {}),
439
+ ...(pkg.peerDependencies || {}),
440
+ }
441
+ : {}
217
442
 
218
443
  const hasSass = allDeps["sass"] || allDeps["node-sass"] || allDeps["sass-loader"]
219
444
  const hasLess = allDeps["less"] || allDeps["less-loader"]
@@ -224,20 +449,34 @@ function detectCssPreprocessor() {
224
449
 
225
450
  // Fallback: check for existing stylelint config
226
451
  if (fileExists(".stylelintrc.cjs") || fileExists(".stylelintrc.js")) {
452
+ const stylelintFile = fileExists(".stylelintrc.cjs")
453
+ ? ".stylelintrc.cjs"
454
+ : ".stylelintrc.js"
455
+ let content = ""
227
456
  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"
457
+ content = fs.readFileSync(path.resolve(PROJECT_ROOT, stylelintFile), "utf8")
237
458
  } catch {}
459
+ // "my-code-style/stylelint" is a prefix of "my-code-style/stylelint/less",
460
+ // so the Less entry must be matched before the SCSS one.
461
+ const usesLess =
462
+ /my-code-style\/stylelint\/less(?:-override)?/.test(content) ||
463
+ content.includes("postcss-less") ||
464
+ content.includes("stylelint-config-recommended-less")
465
+ const usesScss =
466
+ /my-code-style\/stylelint(?![-\w/])/.test(content) ||
467
+ content.includes("postcss-scss") ||
468
+ content.includes("stylelint-config-recommended-scss")
469
+ if (usesLess && usesScss) return "both"
470
+ if (usesLess) return "less"
471
+ if (usesScss) return "scss"
238
472
  return "scss"
239
473
  }
240
474
 
475
+ // 无 package.json 时无法判断项目类型,不猜测样式方案(避免凭空安装一堆 stylelint 依赖)
476
+ if (!pkg) {
477
+ return "none"
478
+ }
479
+
241
480
  // If not a Vue/uni-app project and no css preprocessor found, skip stylelint
242
481
  if (!isVueProject()) {
243
482
  return "none"
@@ -306,6 +545,11 @@ module.exports = require("${importPath}")
306
545
  }
307
546
 
308
547
  function commitlintrcContent() {
548
+ const mockScope = ["mock", "mocks", "src/mock", "src/mocks"].some((dir) =>
549
+ fs.existsSync(path.resolve(PROJECT_ROOT, dir)),
550
+ )
551
+ ? `, "mock"`
552
+ : ""
309
553
  return `// Commitlint config — powered by my-code-style
310
554
  const base = require("my-code-style/commitlint")
311
555
  const { generateScopes, guessCurrentScope } = require("my-code-style/commitlint/scopes")
@@ -319,7 +563,7 @@ module.exports = {
319
563
  ...base.prompt,
320
564
  customScopesAlign: !scopeComplete ? "top" : "bottom",
321
565
  defaultScope: scopeComplete,
322
- scopes: [...scopes, "mock"],
566
+ scopes: [...scopes${mockScope}],
323
567
  allowEmptyIssuePrefixs: false,
324
568
  allowCustomIssuePrefixs: false,
325
569
  },
@@ -333,12 +577,14 @@ module.exports = require("my-code-style/versionrc")
333
577
  `
334
578
  }
335
579
 
580
+ // Hook bodies live in src/husky/* so the shipped templates cannot drift from
581
+ // what the CLI writes. Normalise the trailing newline.
336
582
  function huskyCommitMsg() {
337
- return `npx --no-install commitlint --edit "\${1}"\n`
583
+ return readTemplate("src/husky/commit-msg").replace(/\s*$/, "\n")
338
584
  }
339
585
 
340
586
  function huskyPreCommit() {
341
- return `npx --no-install -- lint-staged\n`
587
+ return readTemplate("src/husky/pre-commit").replace(/\s*$/, "\n")
342
588
  }
343
589
 
344
590
  function editorconfigContent() {
@@ -450,6 +696,7 @@ function main() {
450
696
  }
451
697
 
452
698
  // --- Detection phase ---
699
+ const pm = detectPackageManager() || "pnpm"
453
700
  const eslintVersion = detectEslintVersion()
454
701
  const cssPreprocessor = detectCssPreprocessor()
455
702
  const projectType = detectProjectType()
@@ -470,11 +717,19 @@ function main() {
470
717
  fileExists("eslint.config.cjs")
471
718
 
472
719
  if (eslintVersion !== null && eslintVersion < 8) {
473
- console.error(`不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 或 9,未修改任何文件`)
720
+ console.error(
721
+ `不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 / 9 / 10,未修改任何文件`,
722
+ )
474
723
  process.exitCode = 1
475
724
  return
476
725
  }
477
726
 
727
+ if (eslintVersion !== null && eslintVersion > 10) {
728
+ warn(
729
+ `ESLint ${eslintVersion} 尚未在 peerDependencies 声明支持(^8.57.0 || ^9.0.0 || ^10.0.0),将按 Flat Config 生成`,
730
+ )
731
+ }
732
+
478
733
  // Do not silently mix an installed major version with an incompatible format.
479
734
  if (
480
735
  (eslintVersion >= 9 && hasLegacyConfigFile && !hasFlatConfigFile) ||
@@ -562,7 +817,9 @@ function main() {
562
817
  }
563
818
 
564
819
  if (isEsm && fileExists(".versionrc.js") && !fileExists(".versionrc.cjs")) {
565
- warn("检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs")
820
+ warn(
821
+ "检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs",
822
+ )
566
823
  }
567
824
 
568
825
  files.push({
@@ -632,7 +889,9 @@ function main() {
632
889
  if (backup) {
633
890
  const backupDir = path.resolve(PROJECT_ROOT, ".my-code-style-backup")
634
891
  ensureDir(backupDir)
635
- for (const item of [...files, ...hooks]) {
892
+ // package.json is rewritten (scripts + lint-staged), so it must be backed
893
+ // up as well — users lose custom lint-staged groups otherwise.
894
+ for (const item of [...files, ...hooks, { path: "package.json" }]) {
636
895
  if (fileExists(item.path)) {
637
896
  const srcPath = path.resolve(PROJECT_ROOT, item.path)
638
897
  const targetPath = path.resolve(backupDir, item.path)
@@ -722,6 +981,11 @@ function main() {
722
981
  "lint-staged": getLintStagedConfig(cssPreprocessor),
723
982
  })
724
983
  success("更新 package.json (scripts + lint-staged)")
984
+
985
+ // The backup directory is ours, not the user's source — keep it out of git
986
+ if (backup) {
987
+ ensureGitignoreEntry(".my-code-style-backup/")
988
+ }
725
989
  } catch (err) {
726
990
  console.error(`\n ✖ 初始化写入失败: ${err.message};正在回滚...`)
727
991
  for (const [relPath, info] of snapshot.entries()) {
@@ -764,19 +1028,28 @@ function main() {
764
1028
  console.log("")
765
1029
  console.log(" 下一步:")
766
1030
  console.log(" 1. 确保已安装 peerDependencies:")
767
- console.log(" pnpm add -D my-code-style")
1031
+ console.log(` ${addDevCommand(pm)} my-code-style`)
768
1032
  if (useFlatConfig) {
769
1033
  console.log(" 2. Flat Config 需要的额外依赖:")
770
- console.log(" pnpm add -D typescript-eslint globals eslint-plugin-import-x @eslint/js")
1034
+ console.log(
1035
+ ` ${addDevCommand(pm)} typescript-eslint globals eslint-plugin-import-x @eslint/js`,
1036
+ )
771
1037
  console.log(" 3. 初始化 husky:")
772
- console.log(" pnpm prepare")
1038
+ console.log(` ${runScriptCommand(pm, "prepare")}`)
773
1039
  console.log(" 4. 使用 czg 提交 commit:")
774
1040
  } else {
775
1041
  console.log(" 2. 初始化 husky:")
776
- console.log(" pnpm prepare")
1042
+ console.log(` ${runScriptCommand(pm, "prepare")}`)
777
1043
  console.log(" 3. 使用 czg 提交 commit:")
778
1044
  }
779
- console.log(" pnpm cz")
1045
+ console.log(` ${runScriptCommand(pm, "cz")}`)
1046
+ if (pm === "pnpm") {
1047
+ console.log("")
1048
+ console.log(" 提示:pnpm 10+ 默认拦截依赖的构建脚本,若安装时提示")
1049
+ console.log(
1050
+ " ERR_PNPM_IGNORED_BUILDS(如 unrs-resolver),执行 pnpm approve-builds 后重试。",
1051
+ )
1052
+ }
780
1053
  console.log("")
781
1054
  }
782
1055
 
@@ -787,6 +1060,20 @@ function modifyPackageJson(updates) {
787
1060
  pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
788
1061
  }
789
1062
 
1063
+ // A generated manifest must be installable on its own: npm requires
1064
+ // name/version (e.g. running init in an empty directory).
1065
+ if (typeof pkg.name !== "string" || !pkg.name.trim()) {
1066
+ pkg.name =
1067
+ path
1068
+ .basename(PROJECT_ROOT)
1069
+ .toLowerCase()
1070
+ .replace(/[^a-z0-9._-]+/g, "-")
1071
+ .replace(/^[._-]+|[._-]+$/g, "") || "my-project"
1072
+ }
1073
+ if (typeof pkg.version !== "string" || !pkg.version.trim()) {
1074
+ pkg.version = "1.0.0"
1075
+ }
1076
+
790
1077
  // Merge scripts: updates won't overwrite existing scripts with the same key
791
1078
  if (updates.scripts) {
792
1079
  pkg.scripts = { ...updates.scripts, ...(pkg.scripts || {}) }
@@ -797,7 +1084,14 @@ function modifyPackageJson(updates) {
797
1084
  pkg["lint-staged"] = updates["lint-staged"]
798
1085
  }
799
1086
 
800
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf8")
1087
+ // Keep the project's own indentation (a 2-space repo should not be reflowed)
1088
+ const previous = fileExists("package.json") ? fs.readFileSync(pkgPath, "utf8") : ""
1089
+ const indentMatch = previous.match(/^[ \t]+(?=")/m)
1090
+ fs.writeFileSync(
1091
+ pkgPath,
1092
+ JSON.stringify(pkg, null, indentMatch ? indentMatch[0] : 4) + "\n",
1093
+ "utf8",
1094
+ )
801
1095
  }
802
1096
 
803
1097
  function checkPeerDeps({
@@ -815,6 +1109,8 @@ function checkPeerDeps({
815
1109
 
816
1110
  const baseDeps = [
817
1111
  "eslint",
1112
+ // Required by @typescript-eslint/parser and typescript-eslint at runtime
1113
+ "typescript",
818
1114
  "prettier",
819
1115
  ...(projectType !== "base" ? ["eslint-plugin-vue"] : []),
820
1116
  "@commitlint/cli",
@@ -904,7 +1200,7 @@ function checkPeerDeps({
904
1200
  warn(`以下依赖未安装:${missing.join(", ")}`)
905
1201
  console.log("")
906
1202
  warn("一键安装命令:")
907
- console.log(` pnpm add -D my-code-style \\`)
1203
+ console.log(` ${addDevCommand(detectPackageManager())} my-code-style \\`)
908
1204
  missing.forEach((dep, i) => {
909
1205
  const version =
910
1206
  dep === "eslint" ? (useFlatConfig ? "^9.0.0" : "^8.57.0") : peerDeps[dep] || ""
@@ -915,4 +1211,7 @@ function checkPeerDeps({
915
1211
  }
916
1212
  }
917
1213
 
918
- main()
1214
+ // Only run when executed directly; requiring this file must be side-effect free.
1215
+ if (require.main === module) {
1216
+ main()
1217
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-code-style",
3
- "version": "1.4.0",
3
+ "version": "1.6.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",
@@ -44,10 +45,12 @@
44
45
  "test": "node --test tests/*.test.cjs demo-test/*.test.cjs",
45
46
  "test:demo": "node --test demo-test/*.test.cjs",
46
47
  "test:integration:setup": "node demo-test/setup-integration.cjs",
47
- "test:integration": "node --test demo-test/integration/*.test.cjs",
48
+ "test:integration": "node scripts/test-all.cjs --suite=integration",
48
49
  "test:legacy:setup": "node demo-test/setup-legacy.cjs",
49
- "test:legacy": "node --test demo-test/legacy/*.test.cjs",
50
- "test:all": "npm test && npm run test:integration && npm run test:legacy",
50
+ "test:legacy": "node scripts/test-all.cjs --suite=legacy",
51
+ "test:all": "node scripts/test-all.cjs",
52
+ "diagnose": "node scripts/diagnose-speed.cjs",
53
+ "sweep": "node scripts/sweep-concurrency.cjs",
51
54
  "lint": "eslint .",
52
55
  "lint:fix": "eslint . --fix",
53
56
  "format": "prettier --write .",
@@ -66,12 +69,11 @@
66
69
  "peerDependencies": {
67
70
  "@commitlint/cli": "^19.0.0",
68
71
  "@commitlint/config-conventional": "^19.0.0",
69
- "@eslint/eslintrc": "^3.0.0",
70
72
  "@eslint/js": "^9.0.0",
71
73
  "@typescript-eslint/eslint-plugin": "^8.0.0",
72
74
  "@typescript-eslint/parser": "^8.0.0",
73
75
  "czg": "^1.0.0",
74
- "eslint": "^8.57.0 || ^9.0.0",
76
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
75
77
  "eslint-config-prettier": "^10.1.8",
76
78
  "eslint-import-resolver-typescript": "^3.0.0",
77
79
  "eslint-plugin-import": "^2.0.0",
@@ -93,6 +95,7 @@
93
95
  "stylelint-config-recommended-scss": "^16.0.0",
94
96
  "stylelint-config-recommended-vue": "^2.0.0",
95
97
  "stylelint-prettier": "^5.0.0",
98
+ "typescript": "^5.0.0",
96
99
  "typescript-eslint": "^8.0.0",
97
100
  "vue-eslint-parser": "^10.3.0"
98
101
  },
@@ -103,9 +106,6 @@
103
106
  "@commitlint/config-conventional": {
104
107
  "optional": true
105
108
  },
106
- "@eslint/eslintrc": {
107
- "optional": true
108
- },
109
109
  "@eslint/js": {
110
110
  "optional": true
111
111
  },
@@ -184,6 +184,9 @@
184
184
  "stylelint-prettier": {
185
185
  "optional": true
186
186
  },
187
+ "typescript": {
188
+ "optional": true
189
+ },
187
190
  "typescript-eslint": {
188
191
  "optional": true
189
192
  },