my-code-style 1.2.0 → 1.4.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
@@ -62,42 +62,165 @@ function getProjectPkg() {
62
62
  }
63
63
 
64
64
  function getDependencyVersion(pkg, depName) {
65
- return (pkg.dependencies && pkg.dependencies[depName])
66
- || (pkg.devDependencies && pkg.devDependencies[depName])
67
- || null
65
+ return (
66
+ (pkg.dependencies && pkg.dependencies[depName]) ||
67
+ (pkg.devDependencies && pkg.devDependencies[depName]) ||
68
+ (pkg.peerDependencies && pkg.peerDependencies[depName]) ||
69
+ null
70
+ )
68
71
  }
69
72
 
70
73
  // --- Detection helpers ---
71
74
 
72
75
  /**
73
- * Detect ESLint major version from package.json
74
- * Returns 8, 9, or null (not installed)
76
+ * Detect if project is configured as ECMAScript Module (ESM)
75
77
  */
76
- function detectEslintVersion() {
77
- const pkg = getProjectPkg()
78
+ function isEsmProject(pkg = getProjectPkg()) {
79
+ return pkg?.type === "module"
80
+ }
81
+
82
+ /**
83
+ * Detect ESLint major version from package.json or installed node_modules
84
+ * Returns 8, 9, or null (not installed or ambiguous)
85
+ */
86
+ function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
78
87
  if (!pkg) return null
79
- const version = getDependencyVersion(pkg, "eslint")
80
- if (!version) return null
81
- // Handle range specifiers like "^9.0.0", ">=8.0.0", "9.x"
88
+
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
97
+ }
98
+ }
99
+ } catch {
100
+ // Fall through to package.json dependency declaration
101
+ }
102
+
103
+ // 2. Parse package.json dependency declaration
104
+ let version = getDependencyVersion(pkg, "eslint")
105
+ if (!version || typeof version !== "string") return null
106
+
107
+ version = version.trim()
108
+
109
+ // Handle npm alias: e.g. "npm:eslint@^8.57.0"
110
+ if (version.startsWith("npm:")) {
111
+ const atIdx = version.lastIndexOf("@")
112
+ if (atIdx > 3) {
113
+ version = version.slice(atIdx + 1).trim()
114
+ }
115
+ }
116
+
117
+ // Handle workspace protocol: e.g. "workspace:^8.0.0", "workspace:*"
118
+ if (version.startsWith("workspace:")) {
119
+ version = version.slice("workspace:".length).trim()
120
+ if (version === "*" || version === "^" || version === "~") {
121
+ return null
122
+ }
123
+ }
124
+
125
+ // Unspecified or dynamic ranges
126
+ if (
127
+ version === "*" ||
128
+ version === "" ||
129
+ version.startsWith("catalog:") ||
130
+ version.startsWith("file:") ||
131
+ version.startsWith("link:")
132
+ ) {
133
+ return null
134
+ }
135
+
136
+ // npm tag: latest
137
+ if (version === "latest") {
138
+ return 9
139
+ }
140
+
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
+ }
150
+ }
151
+
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
82
160
  const match = version.match(/(\d+)/)
83
- return match ? parseInt(match[1], 10) : null
161
+ if (match) {
162
+ return parseInt(match[1], 10)
163
+ }
164
+
165
+ return null
166
+ }
167
+
168
+ /**
169
+ * Detect if project is a uni-app project
170
+ */
171
+ function isUniAppProject() {
172
+ const pkg = getProjectPkg()
173
+ if (!pkg) return false
174
+ const allDeps = {
175
+ ...(pkg.dependencies || {}),
176
+ ...(pkg.devDependencies || {}),
177
+ ...(pkg.peerDependencies || {}),
178
+ }
179
+ return !!(allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
180
+ }
181
+
182
+ /**
183
+ * Detect if project is a Vue project
184
+ */
185
+ function isVueProject() {
186
+ const pkg = getProjectPkg()
187
+ if (!pkg) return false
188
+ const allDeps = {
189
+ ...(pkg.dependencies || {}),
190
+ ...(pkg.devDependencies || {}),
191
+ ...(pkg.peerDependencies || {}),
192
+ }
193
+ return !!(allDeps["vue"] || allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
194
+ }
195
+
196
+ /**
197
+ * Detect project type: "uniapp", "vue", or "base"
198
+ */
199
+ function detectProjectType() {
200
+ if (isUniAppProject()) return "uniapp"
201
+ if (isVueProject()) return "vue"
202
+ return "base"
84
203
  }
85
204
 
86
205
  /**
87
206
  * Detect CSS preprocessor in use
88
- * Returns "scss", "less", or "none"
207
+ * Returns "scss", "less", "both", or "none"
89
208
  */
90
209
  function detectCssPreprocessor() {
91
210
  const pkg = getProjectPkg()
92
211
  if (!pkg) return "scss"
93
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
212
+ const allDeps = {
213
+ ...(pkg.dependencies || {}),
214
+ ...(pkg.devDependencies || {}),
215
+ ...(pkg.peerDependencies || {}),
216
+ }
94
217
 
95
218
  const hasSass = allDeps["sass"] || allDeps["node-sass"] || allDeps["sass-loader"]
96
219
  const hasLess = allDeps["less"] || allDeps["less-loader"]
97
220
 
221
+ if (hasSass && hasLess) return "both"
98
222
  if (hasLess && !hasSass) return "less"
99
223
  if (hasSass) return "scss"
100
- if (hasLess) return "less"
101
224
 
102
225
  // Fallback: check for existing stylelint config
103
226
  if (fileExists(".stylelintrc.cjs") || fileExists(".stylelintrc.js")) {
@@ -105,33 +228,33 @@ function detectCssPreprocessor() {
105
228
  const stylelintFile = fileExists(".stylelintrc.cjs")
106
229
  ? ".stylelintrc.cjs"
107
230
  : ".stylelintrc.js"
108
- const content = fs.readFileSync(
109
- path.resolve(PROJECT_ROOT, stylelintFile),
110
- "utf8"
111
- )
112
- if (content.includes("postcss-less")) return "less"
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"
113
237
  } catch {}
238
+ return "scss"
114
239
  }
115
240
 
116
- return "scss" // default
117
- }
241
+ // If not a Vue/uni-app project and no css preprocessor found, skip stylelint
242
+ if (!isVueProject()) {
243
+ return "none"
244
+ }
118
245
 
119
- /**
120
- * Detect if project is a uni-app project
121
- */
122
- function isUniAppProject() {
123
- const pkg = getProjectPkg()
124
- if (!pkg) return false
125
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
126
- return !!(allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
246
+ return "scss" // default for Vue/uni-app projects
127
247
  }
128
248
 
129
249
  // --- Config templates ---
130
250
 
131
- function eslintrcContent(isUniApp) {
132
- const importPath = isUniApp
133
- ? "my-code-style/eslint/uniapp"
134
- : "my-code-style/eslint/vue3"
251
+ function eslintrcContent(projectType) {
252
+ let importPath = "my-code-style/eslint"
253
+ if (projectType === "uniapp") {
254
+ importPath = "my-code-style/eslint/uniapp"
255
+ } else if (projectType === "vue") {
256
+ importPath = "my-code-style/eslint/vue3"
257
+ }
135
258
 
136
259
  return `// ESLint config — powered by my-code-style
137
260
  // https://www.npmjs.com/package/my-code-style
@@ -144,19 +267,22 @@ module.exports = config
144
267
  `
145
268
  }
146
269
 
147
- function eslintFlatConfigContent(isUniApp) {
148
- const importPath = isUniApp
149
- ? "my-code-style/eslint/flat/uniapp"
150
- : "my-code-style/eslint/flat/vue3"
151
- const varName = isUniApp ? "uniappConfig" : "vue3Config"
270
+ function eslintFlatConfigContent(projectType) {
271
+ let importPath = "my-code-style/eslint/flat"
272
+ let varName = "baseConfig"
273
+ if (projectType === "uniapp") {
274
+ importPath = "my-code-style/eslint/flat/uniapp"
275
+ varName = "uniappConfig"
276
+ } else if (projectType === "vue") {
277
+ importPath = "my-code-style/eslint/flat/vue3"
278
+ varName = "vue3Config"
279
+ }
152
280
 
153
281
  return `// ESLint Flat Config — powered by my-code-style
154
282
  // https://www.npmjs.com/package/my-code-style
155
283
  import ${varName} from "${importPath}"
156
284
 
157
- export default [
158
- ...${varName},
159
- ]
285
+ export default [...${varName}]
160
286
  `
161
287
  }
162
288
 
@@ -171,9 +297,8 @@ function prettierignoreContent() {
171
297
  }
172
298
 
173
299
  function stylelintrcContent(cssPreprocessor) {
174
- const importPath = cssPreprocessor === "less"
175
- ? "my-code-style/stylelint/less"
176
- : "my-code-style/stylelint"
300
+ const importPath =
301
+ cssPreprocessor === "less" ? "my-code-style/stylelint/less" : "my-code-style/stylelint"
177
302
 
178
303
  return `// Stylelint config — powered by my-code-style
179
304
  module.exports = require("${importPath}")
@@ -209,19 +334,11 @@ module.exports = require("my-code-style/versionrc")
209
334
  }
210
335
 
211
336
  function huskyCommitMsg() {
212
- return `#!/usr/bin/env sh
213
- . "$(dirname -- "$0")/_/husky.sh"
214
-
215
- npx --no-install commitlint --edit
216
- `
337
+ return `npx --no-install commitlint --edit "\${1}"\n`
217
338
  }
218
339
 
219
340
  function huskyPreCommit() {
220
- return `#!/usr/bin/env sh
221
- . "$(dirname -- "$0")/_/husky.sh"
222
-
223
- npx --no-install -- lint-staged
224
- `
341
+ return `npx --no-install -- lint-staged\n`
225
342
  }
226
343
 
227
344
  function editorconfigContent() {
@@ -232,26 +349,59 @@ function gitattributesContent() {
232
349
  return readTemplate("src/gitattributes")
233
350
  }
234
351
 
352
+ function gitignoreContent() {
353
+ return readTemplate("src/gitignore")
354
+ }
355
+
235
356
  /**
236
357
  * Generate the lint-staged config based on detected CSS preprocessor
237
358
  */
238
359
  function getLintStagedConfig(cssPreprocessor) {
239
- const styleFiles = cssPreprocessor === "less"
240
- ? "**/*.{vue,css,less,html}"
241
- : cssPreprocessor === "none"
242
- ? "**/*.{vue,html}"
243
- : "**/*.{vue,css,scss,html}"
244
-
245
- return {
246
- "**/*.{html,vue,ts,cjs,json,md}": ["prettier --write"],
247
- "**/*.{vue,js,ts,jsx,tsx}": ["eslint --cache --fix"],
248
- [styleFiles]: ["stylelint --fix"],
360
+ // Disjoint groups: a file is owned by exactly one task array.
361
+ // lint-staged runs each array in order, while unrelated groups may run concurrently.
362
+ const componentTasks = ["prettier --write", "eslint --fix"]
363
+ const styleTasks = ["prettier --write"]
364
+ if (cssPreprocessor !== "none") {
365
+ componentTasks.push("stylelint --fix")
366
+ styleTasks.push("stylelint --fix")
367
+ }
368
+ const styleFiles =
369
+ cssPreprocessor === "both"
370
+ ? "**/*.{html,css,scss,less}"
371
+ : cssPreprocessor === "less"
372
+ ? "**/*.{html,css,less}"
373
+ : cssPreprocessor === "scss"
374
+ ? "**/*.{html,css,scss}"
375
+ : "**/*.{html,css}"
376
+ const config = {
377
+ "**/*.{vue,nvue}": componentTasks,
378
+ "**/*.{js,ts,jsx,tsx,cjs,mjs,mts,cts}": ["prettier --write", "eslint --fix"],
379
+ [styleFiles]: styleTasks,
380
+ "**/*.{json,json5,md,yml,yaml}": ["prettier --write"],
249
381
  }
382
+
383
+ return config
250
384
  }
251
385
 
252
386
  // --- Main ---
253
387
 
254
388
  function main() {
389
+ const args = process.argv.slice(2)
390
+ const supported = new Set(["--dry-run", "--backup", "--version", "-v", "--help", "-h"])
391
+ const unknown = args.filter((arg) => !supported.has(arg))
392
+ if (unknown.length) {
393
+ console.error(`未知参数: ${unknown.join(", ")};使用 --help 查看帮助`)
394
+ process.exitCode = 1
395
+ return
396
+ }
397
+ if (args.includes("--help") || args.includes("-h")) {
398
+ console.log("Usage: my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]")
399
+ console.log(" --backup 覆盖已有配置前将其备份到 .my-code-style-backup/ 目录")
400
+ console.log(" --dry-run 试运行,仅输出将执行的变更,不写入任何文件")
401
+ console.log("初始化会覆盖部分配置和 hooks;请先备份并运行 --dry-run。")
402
+ return
403
+ }
404
+
255
405
  // Handle --version / -v
256
406
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
257
407
  const pkg = require("../package.json")
@@ -259,6 +409,34 @@ function main() {
259
409
  return
260
410
  }
261
411
 
412
+ // Validate before detection or any filesystem writes. Missing package.json
413
+ // remains supported, but malformed manifests must never be treated as empty.
414
+ if (fileExists("package.json")) {
415
+ try {
416
+ const pkg = JSON.parse(
417
+ fs.readFileSync(path.resolve(PROJECT_ROOT, "package.json"), "utf8"),
418
+ )
419
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
420
+ throw new Error("package.json 必须是 JSON 对象")
421
+ }
422
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "scripts"]) {
423
+ if (pkg[key] === undefined) continue
424
+ if (
425
+ !pkg[key] ||
426
+ typeof pkg[key] !== "object" ||
427
+ Array.isArray(pkg[key]) ||
428
+ Object.values(pkg[key]).some((value) => typeof value !== "string")
429
+ ) {
430
+ throw new Error(`${key} 必须是值为字符串的对象`)
431
+ }
432
+ }
433
+ } catch (error) {
434
+ console.error(`无法读取有效的 package.json:${error.message};未修改任何文件`)
435
+ process.exitCode = 1
436
+ return
437
+ }
438
+ }
439
+
262
440
  const dryRun = process.argv.includes("--dry-run")
263
441
  const label = dryRun ? "[DRY RUN] " : ""
264
442
 
@@ -274,31 +452,77 @@ function main() {
274
452
  // --- Detection phase ---
275
453
  const eslintVersion = detectEslintVersion()
276
454
  const cssPreprocessor = detectCssPreprocessor()
277
- const uniApp = isUniAppProject()
455
+ const projectType = detectProjectType()
278
456
 
279
457
  // Determine ESLint format
280
- const useFlatConfig = eslintVersion === 9
458
+ const hasLegacyConfigFile =
459
+ fileExists(".eslintrc.js") ||
460
+ fileExists(".eslintrc.cjs") ||
461
+ fileExists(".eslintrc.json") ||
462
+ fileExists(".eslintrc.yaml") ||
463
+ fileExists(".eslintrc.yml") ||
464
+ fileExists(".eslintrc")
465
+
466
+ const hasFlatConfigFile =
467
+ fileExists("eslint.config.mjs") ||
468
+ fileExists("eslint.config.js") ||
469
+ fileExists("eslint.config.ts") ||
470
+ fileExists("eslint.config.cjs")
471
+
472
+ if (eslintVersion !== null && eslintVersion < 8) {
473
+ console.error(`不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 或 9,未修改任何文件`)
474
+ process.exitCode = 1
475
+ return
476
+ }
477
+
478
+ // Do not silently mix an installed major version with an incompatible format.
479
+ if (
480
+ (eslintVersion >= 9 && hasLegacyConfigFile && !hasFlatConfigFile) ||
481
+ (eslintVersion === 8 && hasFlatConfigFile)
482
+ ) {
483
+ console.error("ESLint 版本与已有配置格式冲突;请先手动迁移或确认配置格式,未修改任何文件")
484
+ process.exitCode = 1
485
+ return
486
+ }
487
+ if (!eslintVersion && hasLegacyConfigFile && !hasFlatConfigFile) {
488
+ console.error(
489
+ "检测到传统 ESLint 配置但无法确定版本;请先声明 ESLint 版本或迁移到 Flat Config,未修改任何文件",
490
+ )
491
+ process.exitCode = 1
492
+ return
493
+ }
494
+ const useFlatConfig = eslintVersion !== 8
281
495
  const eslintFormat = useFlatConfig ? "flat" : "eslintrc"
282
496
 
283
- log(`检测到 ESLint 版本: ${eslintVersion || "未知"} → 使用 ${eslintFormat} 格式`)
284
- log(`检测到 CSS 预处理器: ${cssPreprocessor}`)
285
- if (uniApp) log(`检测到 uni-app 项目`)
497
+ log(`检测到 ESLint 版本: ${eslintVersion || "默认最新 (9+)"} → 使用 ${eslintFormat} 格式`)
498
+ log(
499
+ `检测到项目类型: ${projectType === "uniapp" ? "uni-app" : projectType === "vue" ? "Vue" : "Node/TS 基础"}`,
500
+ )
501
+ log(
502
+ `检测到 CSS 预处理器: ${cssPreprocessor === "both" ? "SCSS + Less (混合)" : cssPreprocessor}`,
503
+ )
286
504
  console.log("")
287
505
 
288
506
  // --- Build file list based on detection ---
289
507
  const files = []
290
508
 
291
- if (useFlatConfig) {
509
+ if (hasFlatConfigFile || hasLegacyConfigFile) {
510
+ log("保留已有 ESLint 配置,不生成额外入口")
511
+ } else if (useFlatConfig) {
292
512
  files.push({
293
513
  path: "eslint.config.mjs",
294
- exists: fileExists("eslint.config.mjs") || fileExists("eslint.config.js") || fileExists("eslint.config.ts"),
295
- content: eslintFlatConfigContent(uniApp),
514
+ exists:
515
+ fileExists("eslint.config.mjs") ||
516
+ fileExists("eslint.config.js") ||
517
+ fileExists("eslint.config.ts") ||
518
+ fileExists("eslint.config.cjs"),
519
+ content: eslintFlatConfigContent(projectType),
296
520
  })
297
521
  } else {
298
522
  files.push({
299
523
  path: ".eslintrc.cjs",
300
524
  exists: fileExists(".eslintrc.cjs"),
301
- content: eslintrcContent(uniApp),
525
+ content: eslintrcContent(projectType),
302
526
  })
303
527
  }
304
528
 
@@ -331,9 +555,19 @@ function main() {
331
555
  content: commitlintrcContent(),
332
556
  })
333
557
 
558
+ const isEsm = isEsmProject()
559
+ let versionrcPath = ".versionrc.js"
560
+ if (fileExists(".versionrc.cjs") || isEsm) {
561
+ versionrcPath = ".versionrc.cjs"
562
+ }
563
+
564
+ if (isEsm && fileExists(".versionrc.js") && !fileExists(".versionrc.cjs")) {
565
+ warn("检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs")
566
+ }
567
+
334
568
  files.push({
335
- path: ".versionrc.js",
336
- exists: fileExists(".versionrc.js"),
569
+ path: versionrcPath,
570
+ exists: fileExists(versionrcPath),
337
571
  content: versionrcContent(),
338
572
  })
339
573
 
@@ -349,6 +583,14 @@ function main() {
349
583
  content: gitattributesContent(),
350
584
  })
351
585
 
586
+ if (!fileExists(".gitignore")) {
587
+ files.push({
588
+ path: ".gitignore",
589
+ exists: false,
590
+ content: gitignoreContent(),
591
+ })
592
+ }
593
+
352
594
  // Show what will be created/overwritten
353
595
  for (const f of files) {
354
596
  if (f.exists) {
@@ -385,33 +627,137 @@ function main() {
385
627
  return
386
628
  }
387
629
 
388
- // Actually write files
389
- for (const f of files) {
390
- writeFile(f.path, f.content)
630
+ // Optional backup of existing files before modifying
631
+ const backup = args.includes("--backup")
632
+ if (backup) {
633
+ const backupDir = path.resolve(PROJECT_ROOT, ".my-code-style-backup")
634
+ ensureDir(backupDir)
635
+ for (const item of [...files, ...hooks]) {
636
+ if (fileExists(item.path)) {
637
+ const srcPath = path.resolve(PROJECT_ROOT, item.path)
638
+ const targetPath = path.resolve(backupDir, item.path)
639
+ ensureDir(path.dirname(targetPath))
640
+ fs.copyFileSync(srcPath, targetPath)
641
+ log(`已备份 ${item.path} -> .my-code-style-backup/${item.path}`)
642
+ }
643
+ }
391
644
  }
392
645
 
393
- // Husky
394
- if (!hasHusky) {
395
- ensureDir(huskyDir)
646
+ // ESLint 8 directory traversal defaults to .js; explicitly include typed
647
+ // files and the selected component formats so `lint` cannot silently skip them.
648
+ const legacyExtensions =
649
+ ".js,.cjs,.mjs,.ts,.mts,.cts,.jsx,.tsx" +
650
+ (projectType !== "base" ? ",.vue" : "") +
651
+ (projectType === "uniapp" ? ",.nvue" : "")
652
+ const lintCommand = useFlatConfig ? "eslint ." : `eslint . --ext ${legacyExtensions}`
653
+
654
+ // Take snapshot of files and hooks before making any filesystem modifications
655
+ const snapshot = new Map()
656
+ for (const f of files) {
657
+ const abs = path.resolve(PROJECT_ROOT, f.path)
658
+ let existed = false
659
+ let isFile = false
660
+ let content = null
661
+ try {
662
+ if (fs.existsSync(abs)) {
663
+ existed = true
664
+ isFile = fs.statSync(abs).isFile()
665
+ if (isFile) content = fs.readFileSync(abs, "utf8")
666
+ }
667
+ } catch {}
668
+ snapshot.set(f.path, { existed, isFile, content })
396
669
  }
397
670
  for (const h of hooks) {
398
- writeFile(h.path, h.content)
399
- fs.chmodSync(path.resolve(PROJECT_ROOT, h.path), 0o755)
400
- }
401
-
402
- // package.json scripts + lint-staged
403
- modifyPackageJson({
404
- scripts: {
405
- prepare: "husky",
406
- release: "standard-version",
407
- cz: "czg",
408
- },
409
- "lint-staged": getLintStagedConfig(cssPreprocessor),
410
- })
411
- success("更新 package.json (scripts + lint-staged)")
671
+ const abs = path.resolve(PROJECT_ROOT, h.path)
672
+ let existed = false
673
+ let isFile = false
674
+ let content = null
675
+ let mode = undefined
676
+ try {
677
+ if (fs.existsSync(abs)) {
678
+ existed = true
679
+ const stat = fs.statSync(abs)
680
+ isFile = stat.isFile()
681
+ mode = stat.mode
682
+ if (isFile) content = fs.readFileSync(abs, "utf8")
683
+ }
684
+ } catch {}
685
+ snapshot.set(h.path, { existed, isFile, content, mode })
686
+ }
687
+ const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
688
+ const hadPkg = fs.existsSync(pkgPath)
689
+ let pkgContent = null
690
+ try {
691
+ if (hadPkg && fs.statSync(pkgPath).isFile()) {
692
+ pkgContent = fs.readFileSync(pkgPath, "utf8")
693
+ }
694
+ } catch {}
695
+ const hadHuskyDir = fs.existsSync(huskyDir)
696
+
697
+ try {
698
+ // Actually write files
699
+ for (const f of files) {
700
+ writeFile(f.path, f.content)
701
+ }
702
+
703
+ // Husky
704
+ if (!hasHusky) {
705
+ ensureDir(huskyDir)
706
+ }
707
+ for (const h of hooks) {
708
+ writeFile(h.path, h.content)
709
+ fs.chmodSync(path.resolve(PROJECT_ROOT, h.path), 0o755)
710
+ }
711
+
712
+ // package.json scripts + lint-staged
713
+ modifyPackageJson({
714
+ scripts: {
715
+ lint: lintCommand,
716
+ "lint:fix": `${lintCommand} --fix`,
717
+ format: "prettier --write .",
718
+ prepare: "husky",
719
+ release: "standard-version",
720
+ cz: "czg",
721
+ },
722
+ "lint-staged": getLintStagedConfig(cssPreprocessor),
723
+ })
724
+ success("更新 package.json (scripts + lint-staged)")
725
+ } catch (err) {
726
+ console.error(`\n ✖ 初始化写入失败: ${err.message};正在回滚...`)
727
+ for (const [relPath, info] of snapshot.entries()) {
728
+ const abs = path.resolve(PROJECT_ROOT, relPath)
729
+ try {
730
+ if (info.existed && info.isFile) {
731
+ fs.writeFileSync(abs, info.content, "utf8")
732
+ if (info.mode !== undefined) {
733
+ fs.chmodSync(abs, info.mode)
734
+ }
735
+ } else if (!info.existed && fs.existsSync(abs)) {
736
+ fs.rmSync(abs, { force: true, recursive: true })
737
+ }
738
+ } catch (rollbackErr) {
739
+ console.error(` 回滚文件失败: ${relPath} (${rollbackErr.message})`)
740
+ }
741
+ }
742
+ if (hadPkg) {
743
+ fs.writeFileSync(pkgPath, pkgContent, "utf8")
744
+ } else if (fs.existsSync(pkgPath)) {
745
+ fs.rmSync(pkgPath, { force: true })
746
+ }
747
+ if (!hadHuskyDir && fs.existsSync(huskyDir)) {
748
+ try {
749
+ if (fs.readdirSync(huskyDir).length === 0) {
750
+ fs.rmdirSync(huskyDir)
751
+ }
752
+ } catch {}
753
+ }
754
+ console.error(" ✓ 已成功回滚所有更改,工作区已恢复原始状态。\n")
755
+ process.exitCode = 1
756
+ return
757
+ }
412
758
 
413
759
  // Check for missing peerDependencies
414
- checkPeerDeps({ useFlatConfig, cssPreprocessor })
760
+ checkPeerDeps({ useFlatConfig, cssPreprocessor, projectType })
415
761
 
416
762
  console.log("")
417
763
  success("配置初始化完成!")
@@ -441,25 +787,38 @@ function modifyPackageJson(updates) {
441
787
  pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
442
788
  }
443
789
 
444
- // Merge scripts
790
+ // Merge scripts: updates won't overwrite existing scripts with the same key
445
791
  if (updates.scripts) {
446
- pkg.scripts = { ...pkg.scripts, ...updates.scripts }
792
+ pkg.scripts = { ...updates.scripts, ...(pkg.scripts || {}) }
447
793
  }
448
794
 
449
- // Merge lint-staged
795
+ // Set lint-staged (replace existing lint-staged to avoid stale/duplicate patterns)
450
796
  if (updates["lint-staged"]) {
451
- pkg["lint-staged"] = { ...pkg["lint-staged"], ...updates["lint-staged"] }
797
+ pkg["lint-staged"] = updates["lint-staged"]
452
798
  }
453
799
 
454
800
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf8")
455
801
  }
456
802
 
457
- function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {}) {
803
+ function checkPeerDeps({
804
+ useFlatConfig = false,
805
+ cssPreprocessor = "scss",
806
+ projectType = "base",
807
+ } = {}) {
808
+ const pkg = getProjectPkg()
809
+ if (!pkg) return
810
+
811
+ // If running within the package itself, skip external missing deps check
812
+ if (pkg.name === "my-code-style") {
813
+ return
814
+ }
815
+
458
816
  const baseDeps = [
459
817
  "eslint",
460
818
  "prettier",
461
- "eslint-plugin-vue",
819
+ ...(projectType !== "base" ? ["eslint-plugin-vue"] : []),
462
820
  "@commitlint/cli",
821
+ "@commitlint/config-conventional",
463
822
  "husky",
464
823
  "lint-staged",
465
824
  "czg",
@@ -467,25 +826,73 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
467
826
  ]
468
827
 
469
828
  const flatDeps = useFlatConfig
470
- ? ["typescript-eslint", "vue-eslint-parser", "globals", "eslint-plugin-import-x", "@eslint/js", "eslint-plugin-prettier", "eslint-config-prettier"]
471
- : ["@typescript-eslint/parser", "@typescript-eslint/eslint-plugin", "eslint-plugin-prettier", "eslint-config-prettier"]
472
-
473
- const cssDeps = cssPreprocessor === "scss"
474
- ? ["stylelint", "postcss-scss", "postcss-html"]
475
- : cssPreprocessor === "less"
476
- ? ["stylelint", "postcss-less", "postcss-html"]
477
- : []
829
+ ? [
830
+ "typescript-eslint",
831
+ "eslint-import-resolver-typescript",
832
+ ...(projectType !== "base" ? ["vue-eslint-parser"] : []),
833
+ "globals",
834
+ "eslint-plugin-import-x",
835
+ "@eslint/js",
836
+ "eslint-plugin-prettier",
837
+ "eslint-config-prettier",
838
+ ]
839
+ : [
840
+ "@typescript-eslint/parser",
841
+ "@typescript-eslint/eslint-plugin",
842
+ ...(projectType !== "base" ? ["vue-eslint-parser"] : []),
843
+ "eslint-plugin-import",
844
+ "eslint-import-resolver-typescript",
845
+ "eslint-plugin-prettier",
846
+ "eslint-config-prettier",
847
+ ]
848
+
849
+ const cssDeps =
850
+ cssPreprocessor === "both"
851
+ ? [
852
+ "stylelint",
853
+ "postcss-scss",
854
+ "postcss-less",
855
+ "postcss-html",
856
+ "stylelint-config-recommended",
857
+ "stylelint-config-recommended-scss",
858
+ "stylelint-config-recommended-vue",
859
+ "stylelint-config-html",
860
+ "stylelint-config-recess-order",
861
+ "stylelint-prettier",
862
+ ]
863
+ : cssPreprocessor === "scss"
864
+ ? [
865
+ "stylelint",
866
+ "postcss-scss",
867
+ "postcss-html",
868
+ "stylelint-config-recommended",
869
+ "stylelint-config-recommended-scss",
870
+ "stylelint-config-recommended-vue",
871
+ "stylelint-config-html",
872
+ "stylelint-config-recess-order",
873
+ "stylelint-prettier",
874
+ ]
875
+ : cssPreprocessor === "less"
876
+ ? [
877
+ "stylelint",
878
+ "postcss-less",
879
+ "postcss-html",
880
+ "stylelint-config-recommended",
881
+ "stylelint-config-recommended-vue",
882
+ "stylelint-config-html",
883
+ "stylelint-config-recess-order",
884
+ "stylelint-prettier",
885
+ ]
886
+ : []
478
887
 
479
888
  const allPeerDeps = [...baseDeps, ...flatDeps, ...cssDeps]
480
889
 
481
890
  // Read peer dependency versions from my-code-style package.json
482
- const packagePkg = JSON.parse(fs.readFileSync(path.resolve(PACKAGE_DIR, "package.json"), "utf8"))
891
+ const packagePkg = JSON.parse(
892
+ fs.readFileSync(path.resolve(PACKAGE_DIR, "package.json"), "utf8"),
893
+ )
483
894
  const peerDeps = packagePkg.peerDependencies || {}
484
895
 
485
- const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
486
- if (!fileExists("package.json")) return
487
-
488
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
489
896
  const allDeps = {
490
897
  ...(pkg.dependencies || {}),
491
898
  ...(pkg.devDependencies || {}),
@@ -499,7 +906,8 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
499
906
  warn("一键安装命令:")
500
907
  console.log(` pnpm add -D my-code-style \\`)
501
908
  missing.forEach((dep, i) => {
502
- const version = peerDeps[dep] || ""
909
+ const version =
910
+ dep === "eslint" ? (useFlatConfig ? "^9.0.0" : "^8.57.0") : peerDeps[dep] || ""
503
911
  const depWithVersion = version ? `${dep}@${version}` : dep
504
912
  const suffix = i < missing.length - 1 ? " \\" : ""
505
913
  console.log(` ${depWithVersion}${suffix}`)