my-code-style 1.0.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 ADDED
@@ -0,0 +1,918 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * my-code-style-init CLI
4
+ *
5
+ * Scaffolds lint/format/git config files into the current project.
6
+ * Auto-detects ESLint version and CSS preprocessor for optimal config generation.
7
+ *
8
+ * Usage:
9
+ * npx my-code-style-init
10
+ * npx my-code-style-init --dry-run
11
+ */
12
+
13
+ const fs = require("fs")
14
+ const path = require("path")
15
+ const { execSync } = require("child_process")
16
+
17
+ const PROJECT_ROOT = process.cwd()
18
+ const PACKAGE_DIR = path.resolve(__dirname, "..")
19
+
20
+ // --- Helpers ---
21
+
22
+ function log(msg) {
23
+ console.log(` ${msg}`)
24
+ }
25
+
26
+ function success(msg) {
27
+ console.log(` ✓ ${msg}`)
28
+ }
29
+
30
+ function warn(msg) {
31
+ console.log(` ⚠ ${msg}`)
32
+ }
33
+
34
+ function ensureDir(dirPath) {
35
+ if (!fs.existsSync(dirPath)) {
36
+ fs.mkdirSync(dirPath, { recursive: true })
37
+ }
38
+ }
39
+
40
+ function writeFile(dest, content) {
41
+ const destDir = path.dirname(dest)
42
+ ensureDir(destDir)
43
+ fs.writeFileSync(dest, content, "utf8")
44
+ }
45
+
46
+ function fileExists(relative) {
47
+ return fs.existsSync(path.resolve(PROJECT_ROOT, relative))
48
+ }
49
+
50
+ function readTemplate(relativePath) {
51
+ return fs.readFileSync(path.resolve(PACKAGE_DIR, relativePath), "utf8")
52
+ }
53
+
54
+ function getProjectPkg() {
55
+ const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
56
+ if (!fs.existsSync(pkgPath)) return null
57
+ try {
58
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"))
59
+ } catch {
60
+ return null
61
+ }
62
+ }
63
+
64
+ function getDependencyVersion(pkg, depName) {
65
+ return (
66
+ (pkg.dependencies && pkg.dependencies[depName]) ||
67
+ (pkg.devDependencies && pkg.devDependencies[depName]) ||
68
+ (pkg.peerDependencies && pkg.peerDependencies[depName]) ||
69
+ null
70
+ )
71
+ }
72
+
73
+ // --- Detection helpers ---
74
+
75
+ /**
76
+ * Detect if project is configured as ECMAScript Module (ESM)
77
+ */
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) {
87
+ if (!pkg) return null
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
160
+ const match = version.match(/(\d+)/)
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"
203
+ }
204
+
205
+ /**
206
+ * Detect CSS preprocessor in use
207
+ * Returns "scss", "less", "both", or "none"
208
+ */
209
+ function detectCssPreprocessor() {
210
+ const pkg = getProjectPkg()
211
+ if (!pkg) return "scss"
212
+ const allDeps = {
213
+ ...(pkg.dependencies || {}),
214
+ ...(pkg.devDependencies || {}),
215
+ ...(pkg.peerDependencies || {}),
216
+ }
217
+
218
+ const hasSass = allDeps["sass"] || allDeps["node-sass"] || allDeps["sass-loader"]
219
+ const hasLess = allDeps["less"] || allDeps["less-loader"]
220
+
221
+ if (hasSass && hasLess) return "both"
222
+ if (hasLess && !hasSass) return "less"
223
+ if (hasSass) return "scss"
224
+
225
+ // Fallback: check for existing stylelint config
226
+ if (fileExists(".stylelintrc.cjs") || fileExists(".stylelintrc.js")) {
227
+ 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"
237
+ } catch {}
238
+ return "scss"
239
+ }
240
+
241
+ // If not a Vue/uni-app project and no css preprocessor found, skip stylelint
242
+ if (!isVueProject()) {
243
+ return "none"
244
+ }
245
+
246
+ return "scss" // default for Vue/uni-app projects
247
+ }
248
+
249
+ // --- Config templates ---
250
+
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
+ }
258
+
259
+ return `// ESLint config — powered by my-code-style
260
+ // https://www.npmjs.com/package/my-code-style
261
+ const config = require("${importPath}")
262
+
263
+ // 如果需要引入 unplugin-auto-import 生成的 globals,取消下面的注释:
264
+ // config.extends = [...config.extends, "./.eslintrc-auto-import.json"]
265
+
266
+ module.exports = config
267
+ `
268
+ }
269
+
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
+ }
280
+
281
+ return `// ESLint Flat Config — powered by my-code-style
282
+ // https://www.npmjs.com/package/my-code-style
283
+ import ${varName} from "${importPath}"
284
+
285
+ export default [...${varName}]
286
+ `
287
+ }
288
+
289
+ function prettierrcContent() {
290
+ return `// Prettier config — powered by my-code-style
291
+ module.exports = require("my-code-style/prettier")
292
+ `
293
+ }
294
+
295
+ function prettierignoreContent() {
296
+ return readTemplate("src/prettierignore")
297
+ }
298
+
299
+ function stylelintrcContent(cssPreprocessor) {
300
+ const importPath =
301
+ cssPreprocessor === "less" ? "my-code-style/stylelint/less" : "my-code-style/stylelint"
302
+
303
+ return `// Stylelint config — powered by my-code-style
304
+ module.exports = require("${importPath}")
305
+ `
306
+ }
307
+
308
+ function commitlintrcContent() {
309
+ return `// Commitlint config — powered by my-code-style
310
+ const base = require("my-code-style/commitlint")
311
+ const { generateScopes, guessCurrentScope } = require("my-code-style/commitlint/scopes")
312
+
313
+ const scopes = generateScopes("src")
314
+ const scopeComplete = guessCurrentScope()
315
+
316
+ module.exports = {
317
+ ...base,
318
+ prompt: {
319
+ ...base.prompt,
320
+ customScopesAlign: !scopeComplete ? "top" : "bottom",
321
+ defaultScope: scopeComplete,
322
+ scopes: [...scopes, "mock"],
323
+ allowEmptyIssuePrefixs: false,
324
+ allowCustomIssuePrefixs: false,
325
+ },
326
+ }
327
+ `
328
+ }
329
+
330
+ function versionrcContent() {
331
+ return `// standard-version config — powered by my-code-style
332
+ module.exports = require("my-code-style/versionrc")
333
+ `
334
+ }
335
+
336
+ function huskyCommitMsg() {
337
+ return `npx --no-install commitlint --edit "\${1}"\n`
338
+ }
339
+
340
+ function huskyPreCommit() {
341
+ return `npx --no-install -- lint-staged\n`
342
+ }
343
+
344
+ function editorconfigContent() {
345
+ return readTemplate("src/editorconfig")
346
+ }
347
+
348
+ function gitattributesContent() {
349
+ return readTemplate("src/gitattributes")
350
+ }
351
+
352
+ function gitignoreContent() {
353
+ return readTemplate("src/gitignore")
354
+ }
355
+
356
+ /**
357
+ * Generate the lint-staged config based on detected CSS preprocessor
358
+ */
359
+ function getLintStagedConfig(cssPreprocessor) {
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"],
381
+ }
382
+
383
+ return config
384
+ }
385
+
386
+ // --- Main ---
387
+
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
+
405
+ // Handle --version / -v
406
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
407
+ const pkg = require("../package.json")
408
+ console.log(pkg.version)
409
+ return
410
+ }
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
+
440
+ const dryRun = process.argv.includes("--dry-run")
441
+ const label = dryRun ? "[DRY RUN] " : ""
442
+
443
+ console.log("")
444
+ console.log(" my-code-style-init — 初始化项目配置")
445
+ console.log("")
446
+
447
+ if (dryRun) {
448
+ warn("Dry run mode — no files will be written")
449
+ console.log("")
450
+ }
451
+
452
+ // --- Detection phase ---
453
+ const eslintVersion = detectEslintVersion()
454
+ const cssPreprocessor = detectCssPreprocessor()
455
+ const projectType = detectProjectType()
456
+
457
+ // Determine ESLint format
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
495
+ const eslintFormat = useFlatConfig ? "flat" : "eslintrc"
496
+
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
+ )
504
+ console.log("")
505
+
506
+ // --- Build file list based on detection ---
507
+ const files = []
508
+
509
+ if (hasFlatConfigFile || hasLegacyConfigFile) {
510
+ log("保留已有 ESLint 配置,不生成额外入口")
511
+ } else if (useFlatConfig) {
512
+ files.push({
513
+ path: "eslint.config.mjs",
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),
520
+ })
521
+ } else {
522
+ files.push({
523
+ path: ".eslintrc.cjs",
524
+ exists: fileExists(".eslintrc.cjs"),
525
+ content: eslintrcContent(projectType),
526
+ })
527
+ }
528
+
529
+ files.push({
530
+ path: ".prettierrc.cjs",
531
+ exists: fileExists(".prettierrc.cjs"),
532
+ content: prettierrcContent(),
533
+ })
534
+
535
+ files.push({
536
+ path: ".prettierignore",
537
+ exists: fileExists(".prettierignore"),
538
+ content: prettierignoreContent(),
539
+ })
540
+
541
+ // Stylelint: always generate, but note if no CSS preprocessor
542
+ if (cssPreprocessor !== "none") {
543
+ files.push({
544
+ path: ".stylelintrc.cjs",
545
+ exists: fileExists(".stylelintrc.cjs"),
546
+ content: stylelintrcContent(cssPreprocessor),
547
+ })
548
+ } else {
549
+ log("跳过 Stylelint 配置 (未检测到 CSS 预处理器)")
550
+ }
551
+
552
+ files.push({
553
+ path: ".commitlintrc.cjs",
554
+ exists: fileExists(".commitlintrc.cjs"),
555
+ content: commitlintrcContent(),
556
+ })
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
+
568
+ files.push({
569
+ path: versionrcPath,
570
+ exists: fileExists(versionrcPath),
571
+ content: versionrcContent(),
572
+ })
573
+
574
+ files.push({
575
+ path: ".editorconfig",
576
+ exists: fileExists(".editorconfig"),
577
+ content: editorconfigContent(),
578
+ })
579
+
580
+ files.push({
581
+ path: ".gitattributes",
582
+ exists: fileExists(".gitattributes"),
583
+ content: gitattributesContent(),
584
+ })
585
+
586
+ if (!fileExists(".gitignore")) {
587
+ files.push({
588
+ path: ".gitignore",
589
+ exists: false,
590
+ content: gitignoreContent(),
591
+ })
592
+ }
593
+
594
+ // Show what will be created/overwritten
595
+ for (const f of files) {
596
+ if (f.exists) {
597
+ warn(`${label}覆盖 ${f.path}`)
598
+ } else {
599
+ success(`${label}创建 ${f.path}`)
600
+ }
601
+ }
602
+
603
+ // Husky hooks
604
+ const huskyDir = path.resolve(PROJECT_ROOT, ".husky")
605
+ const hasHusky = fs.existsSync(huskyDir)
606
+ if (!hasHusky) {
607
+ success(`${label}创建 .husky/ 目录`)
608
+ }
609
+
610
+ const hooks = [
611
+ { path: ".husky/commit-msg", content: huskyCommitMsg() },
612
+ { path: ".husky/pre-commit", content: huskyPreCommit() },
613
+ ]
614
+
615
+ for (const h of hooks) {
616
+ if (fileExists(h.path)) {
617
+ warn(`${label}覆盖 ${h.path}`)
618
+ } else {
619
+ success(`${label}创建 ${h.path}`)
620
+ }
621
+ }
622
+
623
+ if (dryRun) {
624
+ console.log("")
625
+ warn("Dry run 结束,未修改任何文件")
626
+ console.log("")
627
+ return
628
+ }
629
+
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
+ }
644
+ }
645
+
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 })
669
+ }
670
+ for (const h of hooks) {
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
+ }
758
+
759
+ // Check for missing peerDependencies
760
+ checkPeerDeps({ useFlatConfig, cssPreprocessor, projectType })
761
+
762
+ console.log("")
763
+ success("配置初始化完成!")
764
+ console.log("")
765
+ console.log(" 下一步:")
766
+ console.log(" 1. 确保已安装 peerDependencies:")
767
+ console.log(" pnpm add -D my-code-style")
768
+ if (useFlatConfig) {
769
+ console.log(" 2. Flat Config 需要的额外依赖:")
770
+ console.log(" pnpm add -D typescript-eslint globals eslint-plugin-import-x @eslint/js")
771
+ console.log(" 3. 初始化 husky:")
772
+ console.log(" pnpm prepare")
773
+ console.log(" 4. 使用 czg 提交 commit:")
774
+ } else {
775
+ console.log(" 2. 初始化 husky:")
776
+ console.log(" pnpm prepare")
777
+ console.log(" 3. 使用 czg 提交 commit:")
778
+ }
779
+ console.log(" pnpm cz")
780
+ console.log("")
781
+ }
782
+
783
+ function modifyPackageJson(updates) {
784
+ const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
785
+ let pkg = {}
786
+ if (fileExists("package.json")) {
787
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
788
+ }
789
+
790
+ // Merge scripts: updates won't overwrite existing scripts with the same key
791
+ if (updates.scripts) {
792
+ pkg.scripts = { ...updates.scripts, ...(pkg.scripts || {}) }
793
+ }
794
+
795
+ // Set lint-staged (replace existing lint-staged to avoid stale/duplicate patterns)
796
+ if (updates["lint-staged"]) {
797
+ pkg["lint-staged"] = updates["lint-staged"]
798
+ }
799
+
800
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf8")
801
+ }
802
+
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
+
816
+ const baseDeps = [
817
+ "eslint",
818
+ "prettier",
819
+ ...(projectType !== "base" ? ["eslint-plugin-vue"] : []),
820
+ "@commitlint/cli",
821
+ "@commitlint/config-conventional",
822
+ "husky",
823
+ "lint-staged",
824
+ "czg",
825
+ "standard-version",
826
+ ]
827
+
828
+ const flatDeps = useFlatConfig
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
+ : []
887
+
888
+ const allPeerDeps = [...baseDeps, ...flatDeps, ...cssDeps]
889
+
890
+ // Read peer dependency versions from my-code-style package.json
891
+ const packagePkg = JSON.parse(
892
+ fs.readFileSync(path.resolve(PACKAGE_DIR, "package.json"), "utf8"),
893
+ )
894
+ const peerDeps = packagePkg.peerDependencies || {}
895
+
896
+ const allDeps = {
897
+ ...(pkg.dependencies || {}),
898
+ ...(pkg.devDependencies || {}),
899
+ }
900
+
901
+ const missing = allPeerDeps.filter((dep) => !allDeps[dep])
902
+ if (missing.length > 0) {
903
+ console.log("")
904
+ warn(`以下依赖未安装:${missing.join(", ")}`)
905
+ console.log("")
906
+ warn("一键安装命令:")
907
+ console.log(` pnpm add -D my-code-style \\`)
908
+ missing.forEach((dep, i) => {
909
+ const version =
910
+ dep === "eslint" ? (useFlatConfig ? "^9.0.0" : "^8.57.0") : peerDeps[dep] || ""
911
+ const depWithVersion = version ? `${dep}@${version}` : dep
912
+ const suffix = i < missing.length - 1 ? " \\" : ""
913
+ console.log(` ${depWithVersion}${suffix}`)
914
+ })
915
+ }
916
+ }
917
+
918
+ main()