my-code-style 1.2.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 +109 -0
- package/LICENSE +21 -0
- package/README.md +131 -46
- package/bin/init +797 -130
- package/package.json +224 -123
- package/src/commitlint/base.cjs +0 -1
- package/src/commitlint/scopes.cjs +199 -30
- package/src/eslint/base.cjs +29 -3
- package/src/eslint/flat/_shared.mjs +11 -9
- package/src/eslint/flat/base.mjs +39 -17
- package/src/eslint/flat/vue3.mjs +12 -7
- package/src/eslint/vue3.cjs +13 -5
- package/src/gitignore +11 -0
- package/src/husky/commit-msg +1 -4
- package/src/husky/pre-commit +0 -3
- package/src/prettier/index.cjs +1 -2
- package/src/stylelint/index.cjs +22 -1
- package/src/stylelint/less.cjs +17 -2
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
|
|
@@ -62,76 +79,381 @@ function getProjectPkg() {
|
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
function getDependencyVersion(pkg, depName) {
|
|
65
|
-
return (
|
|
66
|
-
|
|
67
|
-
||
|
|
82
|
+
return (
|
|
83
|
+
(pkg.dependencies && pkg.dependencies[depName]) ||
|
|
84
|
+
(pkg.devDependencies && pkg.devDependencies[depName]) ||
|
|
85
|
+
(pkg.peerDependencies && pkg.peerDependencies[depName]) ||
|
|
86
|
+
null
|
|
87
|
+
)
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
// --- Detection helpers ---
|
|
71
91
|
|
|
72
92
|
/**
|
|
73
|
-
* Detect
|
|
74
|
-
* Returns 8, 9, or null (not installed)
|
|
93
|
+
* Detect if project is configured as ECMAScript Module (ESM)
|
|
75
94
|
*/
|
|
76
|
-
function
|
|
77
|
-
|
|
95
|
+
function isEsmProject(pkg = getProjectPkg()) {
|
|
96
|
+
return pkg?.type === "module"
|
|
97
|
+
}
|
|
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
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Detect ESLint major version from package.json or installed node_modules
|
|
237
|
+
* Returns 8, 9, or null (not installed or ambiguous)
|
|
238
|
+
*/
|
|
239
|
+
function detectEslintVersion(pkg = getProjectPkg(), rootDir = PROJECT_ROOT) {
|
|
78
240
|
if (!pkg) return null
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
241
|
+
|
|
242
|
+
// 1. If eslint is installed in node_modules, read its actual major version directly
|
|
243
|
+
try {
|
|
244
|
+
const installedPkgPath = path.resolve(rootDir, "node_modules", "eslint", "package.json")
|
|
245
|
+
if (fs.existsSync(installedPkgPath)) {
|
|
246
|
+
const installedPkg = JSON.parse(fs.readFileSync(installedPkgPath, "utf8"))
|
|
247
|
+
const installedMajor = parseInt(installedPkg.version?.split(".")[0], 10)
|
|
248
|
+
if (!Number.isNaN(installedMajor)) {
|
|
249
|
+
return installedMajor
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Fall through to package.json dependency declaration
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 2. Parse package.json dependency declaration
|
|
257
|
+
let version = getDependencyVersion(pkg, "eslint")
|
|
258
|
+
if (!version || typeof version !== "string") return null
|
|
259
|
+
|
|
260
|
+
version = version.trim()
|
|
261
|
+
|
|
262
|
+
// Handle npm alias: e.g. "npm:eslint@^8.57.0"
|
|
263
|
+
if (version.startsWith("npm:")) {
|
|
264
|
+
const atIdx = version.lastIndexOf("@")
|
|
265
|
+
if (atIdx > 3) {
|
|
266
|
+
version = version.slice(atIdx + 1).trim()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Handle workspace protocol: e.g. "workspace:^8.0.0", "workspace:*"
|
|
271
|
+
if (version.startsWith("workspace:")) {
|
|
272
|
+
version = version.slice("workspace:".length).trim()
|
|
273
|
+
if (version === "*" || version === "^" || version === "~") {
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Unspecified or dynamic ranges
|
|
279
|
+
if (
|
|
280
|
+
version === "*" ||
|
|
281
|
+
version === "" ||
|
|
282
|
+
version.startsWith("catalog:") ||
|
|
283
|
+
version.startsWith("file:") ||
|
|
284
|
+
version.startsWith("link:")
|
|
285
|
+
) {
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// npm tag: latest
|
|
290
|
+
if (version === "latest") {
|
|
291
|
+
return 9
|
|
292
|
+
}
|
|
293
|
+
|
|
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
|
|
299
|
+
}
|
|
300
|
+
|
|
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
|
|
306
|
+
}
|
|
307
|
+
if (majors.includes(9)) return 9
|
|
308
|
+
if (majors.includes(8)) return 8
|
|
309
|
+
return majors[0]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Detect if project is a uni-app project
|
|
314
|
+
*/
|
|
315
|
+
function isUniAppProject() {
|
|
316
|
+
const pkg = getProjectPkg()
|
|
317
|
+
if (!pkg) return false
|
|
318
|
+
const allDeps = {
|
|
319
|
+
...(pkg.dependencies || {}),
|
|
320
|
+
...(pkg.devDependencies || {}),
|
|
321
|
+
...(pkg.peerDependencies || {}),
|
|
322
|
+
}
|
|
323
|
+
return !!(allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Detect if project is a Vue project
|
|
328
|
+
*/
|
|
329
|
+
function isVueProject() {
|
|
330
|
+
const pkg = getProjectPkg()
|
|
331
|
+
if (!pkg) return false
|
|
332
|
+
const allDeps = {
|
|
333
|
+
...(pkg.dependencies || {}),
|
|
334
|
+
...(pkg.devDependencies || {}),
|
|
335
|
+
...(pkg.peerDependencies || {}),
|
|
336
|
+
}
|
|
337
|
+
return !!(allDeps["vue"] || allDeps["@dcloudio/uni-app"] || allDeps["uni-app"])
|
|
338
|
+
}
|
|
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
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Detect project type: "uniapp", "vue", or "base"
|
|
388
|
+
*/
|
|
389
|
+
function detectProjectType() {
|
|
390
|
+
if (isUniAppProject()) return "uniapp"
|
|
391
|
+
if (isVueProject()) return "vue"
|
|
392
|
+
return "base"
|
|
84
393
|
}
|
|
85
394
|
|
|
86
395
|
/**
|
|
87
396
|
* Detect CSS preprocessor in use
|
|
88
|
-
* Returns "scss", "less", or "none"
|
|
397
|
+
* Returns "scss", "less", "both", or "none"
|
|
89
398
|
*/
|
|
90
399
|
function detectCssPreprocessor() {
|
|
91
400
|
const pkg = getProjectPkg()
|
|
92
401
|
if (!pkg) return "scss"
|
|
93
|
-
const allDeps = {
|
|
402
|
+
const allDeps = {
|
|
403
|
+
...(pkg.dependencies || {}),
|
|
404
|
+
...(pkg.devDependencies || {}),
|
|
405
|
+
...(pkg.peerDependencies || {}),
|
|
406
|
+
}
|
|
94
407
|
|
|
95
408
|
const hasSass = allDeps["sass"] || allDeps["node-sass"] || allDeps["sass-loader"]
|
|
96
409
|
const hasLess = allDeps["less"] || allDeps["less-loader"]
|
|
97
410
|
|
|
411
|
+
if (hasSass && hasLess) return "both"
|
|
98
412
|
if (hasLess && !hasSass) return "less"
|
|
99
413
|
if (hasSass) return "scss"
|
|
100
|
-
if (hasLess) return "less"
|
|
101
414
|
|
|
102
415
|
// Fallback: check for existing stylelint config
|
|
103
416
|
if (fileExists(".stylelintrc.cjs") || fileExists(".stylelintrc.js")) {
|
|
417
|
+
const stylelintFile = fileExists(".stylelintrc.cjs")
|
|
418
|
+
? ".stylelintrc.cjs"
|
|
419
|
+
: ".stylelintrc.js"
|
|
420
|
+
let content = ""
|
|
104
421
|
try {
|
|
105
|
-
|
|
106
|
-
? ".stylelintrc.cjs"
|
|
107
|
-
: ".stylelintrc.js"
|
|
108
|
-
const content = fs.readFileSync(
|
|
109
|
-
path.resolve(PROJECT_ROOT, stylelintFile),
|
|
110
|
-
"utf8"
|
|
111
|
-
)
|
|
112
|
-
if (content.includes("postcss-less")) return "less"
|
|
422
|
+
content = fs.readFileSync(path.resolve(PROJECT_ROOT, stylelintFile), "utf8")
|
|
113
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"
|
|
437
|
+
return "scss"
|
|
114
438
|
}
|
|
115
439
|
|
|
116
|
-
|
|
117
|
-
|
|
440
|
+
// If not a Vue/uni-app project and no css preprocessor found, skip stylelint
|
|
441
|
+
if (!isVueProject()) {
|
|
442
|
+
return "none"
|
|
443
|
+
}
|
|
118
444
|
|
|
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"])
|
|
445
|
+
return "scss" // default for Vue/uni-app projects
|
|
127
446
|
}
|
|
128
447
|
|
|
129
448
|
// --- Config templates ---
|
|
130
449
|
|
|
131
|
-
function eslintrcContent(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
450
|
+
function eslintrcContent(projectType) {
|
|
451
|
+
let importPath = "my-code-style/eslint"
|
|
452
|
+
if (projectType === "uniapp") {
|
|
453
|
+
importPath = "my-code-style/eslint/uniapp"
|
|
454
|
+
} else if (projectType === "vue") {
|
|
455
|
+
importPath = "my-code-style/eslint/vue3"
|
|
456
|
+
}
|
|
135
457
|
|
|
136
458
|
return `// ESLint config — powered by my-code-style
|
|
137
459
|
// https://www.npmjs.com/package/my-code-style
|
|
@@ -144,19 +466,22 @@ module.exports = config
|
|
|
144
466
|
`
|
|
145
467
|
}
|
|
146
468
|
|
|
147
|
-
function eslintFlatConfigContent(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
469
|
+
function eslintFlatConfigContent(projectType) {
|
|
470
|
+
let importPath = "my-code-style/eslint/flat"
|
|
471
|
+
let varName = "baseConfig"
|
|
472
|
+
if (projectType === "uniapp") {
|
|
473
|
+
importPath = "my-code-style/eslint/flat/uniapp"
|
|
474
|
+
varName = "uniappConfig"
|
|
475
|
+
} else if (projectType === "vue") {
|
|
476
|
+
importPath = "my-code-style/eslint/flat/vue3"
|
|
477
|
+
varName = "vue3Config"
|
|
478
|
+
}
|
|
152
479
|
|
|
153
480
|
return `// ESLint Flat Config — powered by my-code-style
|
|
154
481
|
// https://www.npmjs.com/package/my-code-style
|
|
155
482
|
import ${varName} from "${importPath}"
|
|
156
483
|
|
|
157
|
-
export default [
|
|
158
|
-
...${varName},
|
|
159
|
-
]
|
|
484
|
+
export default [...${varName}]
|
|
160
485
|
`
|
|
161
486
|
}
|
|
162
487
|
|
|
@@ -171,9 +496,8 @@ function prettierignoreContent() {
|
|
|
171
496
|
}
|
|
172
497
|
|
|
173
498
|
function stylelintrcContent(cssPreprocessor) {
|
|
174
|
-
const importPath =
|
|
175
|
-
? "my-code-style/stylelint/less"
|
|
176
|
-
: "my-code-style/stylelint"
|
|
499
|
+
const importPath =
|
|
500
|
+
cssPreprocessor === "less" ? "my-code-style/stylelint/less" : "my-code-style/stylelint"
|
|
177
501
|
|
|
178
502
|
return `// Stylelint config — powered by my-code-style
|
|
179
503
|
module.exports = require("${importPath}")
|
|
@@ -181,6 +505,11 @@ module.exports = require("${importPath}")
|
|
|
181
505
|
}
|
|
182
506
|
|
|
183
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
|
+
: ""
|
|
184
513
|
return `// Commitlint config — powered by my-code-style
|
|
185
514
|
const base = require("my-code-style/commitlint")
|
|
186
515
|
const { generateScopes, guessCurrentScope } = require("my-code-style/commitlint/scopes")
|
|
@@ -194,7 +523,7 @@ module.exports = {
|
|
|
194
523
|
...base.prompt,
|
|
195
524
|
customScopesAlign: !scopeComplete ? "top" : "bottom",
|
|
196
525
|
defaultScope: scopeComplete,
|
|
197
|
-
scopes: [...scopes
|
|
526
|
+
scopes: [...scopes${mockScope}],
|
|
198
527
|
allowEmptyIssuePrefixs: false,
|
|
199
528
|
allowCustomIssuePrefixs: false,
|
|
200
529
|
},
|
|
@@ -208,20 +537,14 @@ module.exports = require("my-code-style/versionrc")
|
|
|
208
537
|
`
|
|
209
538
|
}
|
|
210
539
|
|
|
540
|
+
// Hook bodies live in src/husky/* so the shipped templates cannot drift from
|
|
541
|
+
// what the CLI writes. Normalise the trailing newline.
|
|
211
542
|
function huskyCommitMsg() {
|
|
212
|
-
return
|
|
213
|
-
. "$(dirname -- "$0")/_/husky.sh"
|
|
214
|
-
|
|
215
|
-
npx --no-install commitlint --edit
|
|
216
|
-
`
|
|
543
|
+
return readTemplate("src/husky/commit-msg").replace(/\s*$/, "\n")
|
|
217
544
|
}
|
|
218
545
|
|
|
219
546
|
function huskyPreCommit() {
|
|
220
|
-
return
|
|
221
|
-
. "$(dirname -- "$0")/_/husky.sh"
|
|
222
|
-
|
|
223
|
-
npx --no-install -- lint-staged
|
|
224
|
-
`
|
|
547
|
+
return readTemplate("src/husky/pre-commit").replace(/\s*$/, "\n")
|
|
225
548
|
}
|
|
226
549
|
|
|
227
550
|
function editorconfigContent() {
|
|
@@ -232,26 +555,59 @@ function gitattributesContent() {
|
|
|
232
555
|
return readTemplate("src/gitattributes")
|
|
233
556
|
}
|
|
234
557
|
|
|
558
|
+
function gitignoreContent() {
|
|
559
|
+
return readTemplate("src/gitignore")
|
|
560
|
+
}
|
|
561
|
+
|
|
235
562
|
/**
|
|
236
563
|
* Generate the lint-staged config based on detected CSS preprocessor
|
|
237
564
|
*/
|
|
238
565
|
function getLintStagedConfig(cssPreprocessor) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"**/*.{html,vue,ts,cjs,json,md}": ["prettier --write"],
|
|
247
|
-
"**/*.{vue,js,ts,jsx,tsx}": ["eslint --cache --fix"],
|
|
248
|
-
[styleFiles]: ["stylelint --fix"],
|
|
566
|
+
// Disjoint groups: a file is owned by exactly one task array.
|
|
567
|
+
// lint-staged runs each array in order, while unrelated groups may run concurrently.
|
|
568
|
+
const componentTasks = ["prettier --write", "eslint --fix"]
|
|
569
|
+
const styleTasks = ["prettier --write"]
|
|
570
|
+
if (cssPreprocessor !== "none") {
|
|
571
|
+
componentTasks.push("stylelint --fix")
|
|
572
|
+
styleTasks.push("stylelint --fix")
|
|
249
573
|
}
|
|
574
|
+
const styleFiles =
|
|
575
|
+
cssPreprocessor === "both"
|
|
576
|
+
? "**/*.{html,css,scss,less}"
|
|
577
|
+
: cssPreprocessor === "less"
|
|
578
|
+
? "**/*.{html,css,less}"
|
|
579
|
+
: cssPreprocessor === "scss"
|
|
580
|
+
? "**/*.{html,css,scss}"
|
|
581
|
+
: "**/*.{html,css}"
|
|
582
|
+
const config = {
|
|
583
|
+
"**/*.{vue,nvue}": componentTasks,
|
|
584
|
+
"**/*.{js,ts,jsx,tsx,cjs,mjs,mts,cts}": ["prettier --write", "eslint --fix"],
|
|
585
|
+
[styleFiles]: styleTasks,
|
|
586
|
+
"**/*.{json,json5,md,yml,yaml}": ["prettier --write"],
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return config
|
|
250
590
|
}
|
|
251
591
|
|
|
252
592
|
// --- Main ---
|
|
253
593
|
|
|
254
594
|
function main() {
|
|
595
|
+
const args = process.argv.slice(2)
|
|
596
|
+
const supported = new Set(["--dry-run", "--backup", "--version", "-v", "--help", "-h"])
|
|
597
|
+
const unknown = args.filter((arg) => !supported.has(arg))
|
|
598
|
+
if (unknown.length) {
|
|
599
|
+
console.error(`未知参数: ${unknown.join(", ")};使用 --help 查看帮助`)
|
|
600
|
+
process.exitCode = 1
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
604
|
+
console.log("Usage: my-code-style-init [--dry-run] [--backup] [--version|-v] [--help|-h]")
|
|
605
|
+
console.log(" --backup 覆盖已有配置前将其备份到 .my-code-style-backup/ 目录")
|
|
606
|
+
console.log(" --dry-run 试运行,仅输出将执行的变更,不写入任何文件")
|
|
607
|
+
console.log("初始化会覆盖部分配置和 hooks;请先备份并运行 --dry-run。")
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
|
|
255
611
|
// Handle --version / -v
|
|
256
612
|
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
257
613
|
const pkg = require("../package.json")
|
|
@@ -259,6 +615,34 @@ function main() {
|
|
|
259
615
|
return
|
|
260
616
|
}
|
|
261
617
|
|
|
618
|
+
// Validate before detection or any filesystem writes. Missing package.json
|
|
619
|
+
// remains supported, but malformed manifests must never be treated as empty.
|
|
620
|
+
if (fileExists("package.json")) {
|
|
621
|
+
try {
|
|
622
|
+
const pkg = JSON.parse(
|
|
623
|
+
fs.readFileSync(path.resolve(PROJECT_ROOT, "package.json"), "utf8"),
|
|
624
|
+
)
|
|
625
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
|
|
626
|
+
throw new Error("package.json 必须是 JSON 对象")
|
|
627
|
+
}
|
|
628
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies", "scripts"]) {
|
|
629
|
+
if (pkg[key] === undefined) continue
|
|
630
|
+
if (
|
|
631
|
+
!pkg[key] ||
|
|
632
|
+
typeof pkg[key] !== "object" ||
|
|
633
|
+
Array.isArray(pkg[key]) ||
|
|
634
|
+
Object.values(pkg[key]).some((value) => typeof value !== "string")
|
|
635
|
+
) {
|
|
636
|
+
throw new Error(`${key} 必须是值为字符串的对象`)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error(`无法读取有效的 package.json:${error.message};未修改任何文件`)
|
|
641
|
+
process.exitCode = 1
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
262
646
|
const dryRun = process.argv.includes("--dry-run")
|
|
263
647
|
const label = dryRun ? "[DRY RUN] " : ""
|
|
264
648
|
|
|
@@ -272,33 +656,88 @@ function main() {
|
|
|
272
656
|
}
|
|
273
657
|
|
|
274
658
|
// --- Detection phase ---
|
|
659
|
+
const pm = detectPackageManager() || "pnpm"
|
|
275
660
|
const eslintVersion = detectEslintVersion()
|
|
276
661
|
const cssPreprocessor = detectCssPreprocessor()
|
|
277
|
-
const
|
|
662
|
+
const projectType = detectProjectType()
|
|
278
663
|
|
|
279
664
|
// Determine ESLint format
|
|
280
|
-
const
|
|
665
|
+
const hasLegacyConfigFile =
|
|
666
|
+
fileExists(".eslintrc.js") ||
|
|
667
|
+
fileExists(".eslintrc.cjs") ||
|
|
668
|
+
fileExists(".eslintrc.json") ||
|
|
669
|
+
fileExists(".eslintrc.yaml") ||
|
|
670
|
+
fileExists(".eslintrc.yml") ||
|
|
671
|
+
fileExists(".eslintrc")
|
|
672
|
+
|
|
673
|
+
const hasFlatConfigFile =
|
|
674
|
+
fileExists("eslint.config.mjs") ||
|
|
675
|
+
fileExists("eslint.config.js") ||
|
|
676
|
+
fileExists("eslint.config.ts") ||
|
|
677
|
+
fileExists("eslint.config.cjs")
|
|
678
|
+
|
|
679
|
+
if (eslintVersion !== null && eslintVersion < 8) {
|
|
680
|
+
console.error(
|
|
681
|
+
`不支持 ESLint ${eslintVersion} 版本;本配置包仅支持 ESLint 8 或 9,未修改任何文件`,
|
|
682
|
+
)
|
|
683
|
+
process.exitCode = 1
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (eslintVersion !== null && eslintVersion >= 10) {
|
|
688
|
+
warn(
|
|
689
|
+
`ESLint ${eslintVersion} 尚未在 peerDependencies 声明支持(^8.57.0 || ^9.0.0),将按 Flat Config 生成`,
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Do not silently mix an installed major version with an incompatible format.
|
|
694
|
+
if (
|
|
695
|
+
(eslintVersion >= 9 && hasLegacyConfigFile && !hasFlatConfigFile) ||
|
|
696
|
+
(eslintVersion === 8 && hasFlatConfigFile)
|
|
697
|
+
) {
|
|
698
|
+
console.error("ESLint 版本与已有配置格式冲突;请先手动迁移或确认配置格式,未修改任何文件")
|
|
699
|
+
process.exitCode = 1
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if (!eslintVersion && hasLegacyConfigFile && !hasFlatConfigFile) {
|
|
703
|
+
console.error(
|
|
704
|
+
"检测到传统 ESLint 配置但无法确定版本;请先声明 ESLint 版本或迁移到 Flat Config,未修改任何文件",
|
|
705
|
+
)
|
|
706
|
+
process.exitCode = 1
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
const useFlatConfig = eslintVersion !== 8
|
|
281
710
|
const eslintFormat = useFlatConfig ? "flat" : "eslintrc"
|
|
282
711
|
|
|
283
|
-
log(`检测到 ESLint 版本: ${eslintVersion || "
|
|
284
|
-
log(
|
|
285
|
-
|
|
712
|
+
log(`检测到 ESLint 版本: ${eslintVersion || "默认最新 (9+)"} → 使用 ${eslintFormat} 格式`)
|
|
713
|
+
log(
|
|
714
|
+
`检测到项目类型: ${projectType === "uniapp" ? "uni-app" : projectType === "vue" ? "Vue" : "Node/TS 基础"}`,
|
|
715
|
+
)
|
|
716
|
+
log(
|
|
717
|
+
`检测到 CSS 预处理器: ${cssPreprocessor === "both" ? "SCSS + Less (混合)" : cssPreprocessor}`,
|
|
718
|
+
)
|
|
286
719
|
console.log("")
|
|
287
720
|
|
|
288
721
|
// --- Build file list based on detection ---
|
|
289
722
|
const files = []
|
|
290
723
|
|
|
291
|
-
if (
|
|
724
|
+
if (hasFlatConfigFile || hasLegacyConfigFile) {
|
|
725
|
+
log("保留已有 ESLint 配置,不生成额外入口")
|
|
726
|
+
} else if (useFlatConfig) {
|
|
292
727
|
files.push({
|
|
293
728
|
path: "eslint.config.mjs",
|
|
294
|
-
exists:
|
|
295
|
-
|
|
729
|
+
exists:
|
|
730
|
+
fileExists("eslint.config.mjs") ||
|
|
731
|
+
fileExists("eslint.config.js") ||
|
|
732
|
+
fileExists("eslint.config.ts") ||
|
|
733
|
+
fileExists("eslint.config.cjs"),
|
|
734
|
+
content: eslintFlatConfigContent(projectType),
|
|
296
735
|
})
|
|
297
736
|
} else {
|
|
298
737
|
files.push({
|
|
299
738
|
path: ".eslintrc.cjs",
|
|
300
739
|
exists: fileExists(".eslintrc.cjs"),
|
|
301
|
-
content: eslintrcContent(
|
|
740
|
+
content: eslintrcContent(projectType),
|
|
302
741
|
})
|
|
303
742
|
}
|
|
304
743
|
|
|
@@ -331,9 +770,21 @@ function main() {
|
|
|
331
770
|
content: commitlintrcContent(),
|
|
332
771
|
})
|
|
333
772
|
|
|
773
|
+
const isEsm = isEsmProject()
|
|
774
|
+
let versionrcPath = ".versionrc.js"
|
|
775
|
+
if (fileExists(".versionrc.cjs") || isEsm) {
|
|
776
|
+
versionrcPath = ".versionrc.cjs"
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (isEsm && fileExists(".versionrc.js") && !fileExists(".versionrc.cjs")) {
|
|
780
|
+
warn(
|
|
781
|
+
"检测到 ESM 项目 (type: module) 存在旧的 .versionrc.js,standard-version 需使用 .versionrc.cjs",
|
|
782
|
+
)
|
|
783
|
+
}
|
|
784
|
+
|
|
334
785
|
files.push({
|
|
335
|
-
path:
|
|
336
|
-
exists: fileExists(
|
|
786
|
+
path: versionrcPath,
|
|
787
|
+
exists: fileExists(versionrcPath),
|
|
337
788
|
content: versionrcContent(),
|
|
338
789
|
})
|
|
339
790
|
|
|
@@ -349,6 +800,14 @@ function main() {
|
|
|
349
800
|
content: gitattributesContent(),
|
|
350
801
|
})
|
|
351
802
|
|
|
803
|
+
if (!fileExists(".gitignore")) {
|
|
804
|
+
files.push({
|
|
805
|
+
path: ".gitignore",
|
|
806
|
+
exists: false,
|
|
807
|
+
content: gitignoreContent(),
|
|
808
|
+
})
|
|
809
|
+
}
|
|
810
|
+
|
|
352
811
|
// Show what will be created/overwritten
|
|
353
812
|
for (const f of files) {
|
|
354
813
|
if (f.exists) {
|
|
@@ -385,52 +844,172 @@ function main() {
|
|
|
385
844
|
return
|
|
386
845
|
}
|
|
387
846
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
847
|
+
// Optional backup of existing files before modifying
|
|
848
|
+
const backup = args.includes("--backup")
|
|
849
|
+
if (backup) {
|
|
850
|
+
const backupDir = path.resolve(PROJECT_ROOT, ".my-code-style-backup")
|
|
851
|
+
ensureDir(backupDir)
|
|
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" }]) {
|
|
855
|
+
if (fileExists(item.path)) {
|
|
856
|
+
const srcPath = path.resolve(PROJECT_ROOT, item.path)
|
|
857
|
+
const targetPath = path.resolve(backupDir, item.path)
|
|
858
|
+
ensureDir(path.dirname(targetPath))
|
|
859
|
+
fs.copyFileSync(srcPath, targetPath)
|
|
860
|
+
log(`已备份 ${item.path} -> .my-code-style-backup/${item.path}`)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
391
863
|
}
|
|
392
864
|
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
865
|
+
// ESLint 8 directory traversal defaults to .js; explicitly include typed
|
|
866
|
+
// files and the selected component formats so `lint` cannot silently skip them.
|
|
867
|
+
const legacyExtensions =
|
|
868
|
+
".js,.cjs,.mjs,.ts,.mts,.cts,.jsx,.tsx" +
|
|
869
|
+
(projectType !== "base" ? ",.vue" : "") +
|
|
870
|
+
(projectType === "uniapp" ? ",.nvue" : "")
|
|
871
|
+
const lintCommand = useFlatConfig ? "eslint ." : `eslint . --ext ${legacyExtensions}`
|
|
872
|
+
|
|
873
|
+
// Take snapshot of files and hooks before making any filesystem modifications
|
|
874
|
+
const snapshot = new Map()
|
|
875
|
+
for (const f of files) {
|
|
876
|
+
const abs = path.resolve(PROJECT_ROOT, f.path)
|
|
877
|
+
let existed = false
|
|
878
|
+
let isFile = false
|
|
879
|
+
let content = null
|
|
880
|
+
try {
|
|
881
|
+
if (fs.existsSync(abs)) {
|
|
882
|
+
existed = true
|
|
883
|
+
isFile = fs.statSync(abs).isFile()
|
|
884
|
+
if (isFile) content = fs.readFileSync(abs, "utf8")
|
|
885
|
+
}
|
|
886
|
+
} catch {}
|
|
887
|
+
snapshot.set(f.path, { existed, isFile, content })
|
|
396
888
|
}
|
|
397
889
|
for (const h of hooks) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
890
|
+
const abs = path.resolve(PROJECT_ROOT, h.path)
|
|
891
|
+
let existed = false
|
|
892
|
+
let isFile = false
|
|
893
|
+
let content = null
|
|
894
|
+
let mode = undefined
|
|
895
|
+
try {
|
|
896
|
+
if (fs.existsSync(abs)) {
|
|
897
|
+
existed = true
|
|
898
|
+
const stat = fs.statSync(abs)
|
|
899
|
+
isFile = stat.isFile()
|
|
900
|
+
mode = stat.mode
|
|
901
|
+
if (isFile) content = fs.readFileSync(abs, "utf8")
|
|
902
|
+
}
|
|
903
|
+
} catch {}
|
|
904
|
+
snapshot.set(h.path, { existed, isFile, content, mode })
|
|
905
|
+
}
|
|
906
|
+
const pkgPath = path.resolve(PROJECT_ROOT, "package.json")
|
|
907
|
+
const hadPkg = fs.existsSync(pkgPath)
|
|
908
|
+
let pkgContent = null
|
|
909
|
+
try {
|
|
910
|
+
if (hadPkg && fs.statSync(pkgPath).isFile()) {
|
|
911
|
+
pkgContent = fs.readFileSync(pkgPath, "utf8")
|
|
912
|
+
}
|
|
913
|
+
} catch {}
|
|
914
|
+
const hadHuskyDir = fs.existsSync(huskyDir)
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
// Actually write files
|
|
918
|
+
for (const f of files) {
|
|
919
|
+
writeFile(f.path, f.content)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Husky
|
|
923
|
+
if (!hasHusky) {
|
|
924
|
+
ensureDir(huskyDir)
|
|
925
|
+
}
|
|
926
|
+
for (const h of hooks) {
|
|
927
|
+
writeFile(h.path, h.content)
|
|
928
|
+
fs.chmodSync(path.resolve(PROJECT_ROOT, h.path), 0o755)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// package.json scripts + lint-staged
|
|
932
|
+
modifyPackageJson({
|
|
933
|
+
scripts: {
|
|
934
|
+
lint: lintCommand,
|
|
935
|
+
"lint:fix": `${lintCommand} --fix`,
|
|
936
|
+
format: "prettier --write .",
|
|
937
|
+
prepare: "husky",
|
|
938
|
+
release: "standard-version",
|
|
939
|
+
cz: "czg",
|
|
940
|
+
},
|
|
941
|
+
"lint-staged": getLintStagedConfig(cssPreprocessor),
|
|
942
|
+
})
|
|
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
|
+
}
|
|
949
|
+
} catch (err) {
|
|
950
|
+
console.error(`\n ✖ 初始化写入失败: ${err.message};正在回滚...`)
|
|
951
|
+
for (const [relPath, info] of snapshot.entries()) {
|
|
952
|
+
const abs = path.resolve(PROJECT_ROOT, relPath)
|
|
953
|
+
try {
|
|
954
|
+
if (info.existed && info.isFile) {
|
|
955
|
+
fs.writeFileSync(abs, info.content, "utf8")
|
|
956
|
+
if (info.mode !== undefined) {
|
|
957
|
+
fs.chmodSync(abs, info.mode)
|
|
958
|
+
}
|
|
959
|
+
} else if (!info.existed && fs.existsSync(abs)) {
|
|
960
|
+
fs.rmSync(abs, { force: true, recursive: true })
|
|
961
|
+
}
|
|
962
|
+
} catch (rollbackErr) {
|
|
963
|
+
console.error(` 回滚文件失败: ${relPath} (${rollbackErr.message})`)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (hadPkg) {
|
|
967
|
+
fs.writeFileSync(pkgPath, pkgContent, "utf8")
|
|
968
|
+
} else if (fs.existsSync(pkgPath)) {
|
|
969
|
+
fs.rmSync(pkgPath, { force: true })
|
|
970
|
+
}
|
|
971
|
+
if (!hadHuskyDir && fs.existsSync(huskyDir)) {
|
|
972
|
+
try {
|
|
973
|
+
if (fs.readdirSync(huskyDir).length === 0) {
|
|
974
|
+
fs.rmdirSync(huskyDir)
|
|
975
|
+
}
|
|
976
|
+
} catch {}
|
|
977
|
+
}
|
|
978
|
+
console.error(" ✓ 已成功回滚所有更改,工作区已恢复原始状态。\n")
|
|
979
|
+
process.exitCode = 1
|
|
980
|
+
return
|
|
981
|
+
}
|
|
412
982
|
|
|
413
983
|
// Check for missing peerDependencies
|
|
414
|
-
checkPeerDeps({ useFlatConfig, cssPreprocessor })
|
|
984
|
+
checkPeerDeps({ useFlatConfig, cssPreprocessor, projectType })
|
|
415
985
|
|
|
416
986
|
console.log("")
|
|
417
987
|
success("配置初始化完成!")
|
|
418
988
|
console.log("")
|
|
419
989
|
console.log(" 下一步:")
|
|
420
990
|
console.log(" 1. 确保已安装 peerDependencies:")
|
|
421
|
-
console.log(
|
|
991
|
+
console.log(` ${addDevCommand(pm)} my-code-style`)
|
|
422
992
|
if (useFlatConfig) {
|
|
423
993
|
console.log(" 2. Flat Config 需要的额外依赖:")
|
|
424
|
-
console.log(
|
|
994
|
+
console.log(
|
|
995
|
+
` ${addDevCommand(pm)} typescript-eslint globals eslint-plugin-import-x @eslint/js`,
|
|
996
|
+
)
|
|
425
997
|
console.log(" 3. 初始化 husky:")
|
|
426
|
-
console.log(
|
|
998
|
+
console.log(` ${runScriptCommand(pm, "prepare")}`)
|
|
427
999
|
console.log(" 4. 使用 czg 提交 commit:")
|
|
428
1000
|
} else {
|
|
429
1001
|
console.log(" 2. 初始化 husky:")
|
|
430
|
-
console.log(
|
|
1002
|
+
console.log(` ${runScriptCommand(pm, "prepare")}`)
|
|
431
1003
|
console.log(" 3. 使用 czg 提交 commit:")
|
|
432
1004
|
}
|
|
433
|
-
console.log(
|
|
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
|
+
}
|
|
434
1013
|
console.log("")
|
|
435
1014
|
}
|
|
436
1015
|
|
|
@@ -441,25 +1020,61 @@ function modifyPackageJson(updates) {
|
|
|
441
1020
|
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
442
1021
|
}
|
|
443
1022
|
|
|
444
|
-
//
|
|
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
|
+
|
|
1037
|
+
// Merge scripts: updates won't overwrite existing scripts with the same key
|
|
445
1038
|
if (updates.scripts) {
|
|
446
|
-
pkg.scripts = { ...
|
|
1039
|
+
pkg.scripts = { ...updates.scripts, ...(pkg.scripts || {}) }
|
|
447
1040
|
}
|
|
448
1041
|
|
|
449
|
-
//
|
|
1042
|
+
// Set lint-staged (replace existing lint-staged to avoid stale/duplicate patterns)
|
|
450
1043
|
if (updates["lint-staged"]) {
|
|
451
|
-
pkg["lint-staged"] =
|
|
1044
|
+
pkg["lint-staged"] = updates["lint-staged"]
|
|
452
1045
|
}
|
|
453
1046
|
|
|
454
|
-
|
|
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
|
+
)
|
|
455
1055
|
}
|
|
456
1056
|
|
|
457
|
-
function checkPeerDeps({
|
|
1057
|
+
function checkPeerDeps({
|
|
1058
|
+
useFlatConfig = false,
|
|
1059
|
+
cssPreprocessor = "scss",
|
|
1060
|
+
projectType = "base",
|
|
1061
|
+
} = {}) {
|
|
1062
|
+
const pkg = getProjectPkg()
|
|
1063
|
+
if (!pkg) return
|
|
1064
|
+
|
|
1065
|
+
// If running within the package itself, skip external missing deps check
|
|
1066
|
+
if (pkg.name === "my-code-style") {
|
|
1067
|
+
return
|
|
1068
|
+
}
|
|
1069
|
+
|
|
458
1070
|
const baseDeps = [
|
|
459
1071
|
"eslint",
|
|
1072
|
+
// Required by @typescript-eslint/parser and typescript-eslint at runtime
|
|
1073
|
+
"typescript",
|
|
460
1074
|
"prettier",
|
|
461
|
-
"eslint-plugin-vue",
|
|
1075
|
+
...(projectType !== "base" ? ["eslint-plugin-vue"] : []),
|
|
462
1076
|
"@commitlint/cli",
|
|
1077
|
+
"@commitlint/config-conventional",
|
|
463
1078
|
"husky",
|
|
464
1079
|
"lint-staged",
|
|
465
1080
|
"czg",
|
|
@@ -467,25 +1082,73 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
|
|
|
467
1082
|
]
|
|
468
1083
|
|
|
469
1084
|
const flatDeps = useFlatConfig
|
|
470
|
-
? [
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1085
|
+
? [
|
|
1086
|
+
"typescript-eslint",
|
|
1087
|
+
"eslint-import-resolver-typescript",
|
|
1088
|
+
...(projectType !== "base" ? ["vue-eslint-parser"] : []),
|
|
1089
|
+
"globals",
|
|
1090
|
+
"eslint-plugin-import-x",
|
|
1091
|
+
"@eslint/js",
|
|
1092
|
+
"eslint-plugin-prettier",
|
|
1093
|
+
"eslint-config-prettier",
|
|
1094
|
+
]
|
|
1095
|
+
: [
|
|
1096
|
+
"@typescript-eslint/parser",
|
|
1097
|
+
"@typescript-eslint/eslint-plugin",
|
|
1098
|
+
...(projectType !== "base" ? ["vue-eslint-parser"] : []),
|
|
1099
|
+
"eslint-plugin-import",
|
|
1100
|
+
"eslint-import-resolver-typescript",
|
|
1101
|
+
"eslint-plugin-prettier",
|
|
1102
|
+
"eslint-config-prettier",
|
|
1103
|
+
]
|
|
1104
|
+
|
|
1105
|
+
const cssDeps =
|
|
1106
|
+
cssPreprocessor === "both"
|
|
1107
|
+
? [
|
|
1108
|
+
"stylelint",
|
|
1109
|
+
"postcss-scss",
|
|
1110
|
+
"postcss-less",
|
|
1111
|
+
"postcss-html",
|
|
1112
|
+
"stylelint-config-recommended",
|
|
1113
|
+
"stylelint-config-recommended-scss",
|
|
1114
|
+
"stylelint-config-recommended-vue",
|
|
1115
|
+
"stylelint-config-html",
|
|
1116
|
+
"stylelint-config-recess-order",
|
|
1117
|
+
"stylelint-prettier",
|
|
1118
|
+
]
|
|
1119
|
+
: cssPreprocessor === "scss"
|
|
1120
|
+
? [
|
|
1121
|
+
"stylelint",
|
|
1122
|
+
"postcss-scss",
|
|
1123
|
+
"postcss-html",
|
|
1124
|
+
"stylelint-config-recommended",
|
|
1125
|
+
"stylelint-config-recommended-scss",
|
|
1126
|
+
"stylelint-config-recommended-vue",
|
|
1127
|
+
"stylelint-config-html",
|
|
1128
|
+
"stylelint-config-recess-order",
|
|
1129
|
+
"stylelint-prettier",
|
|
1130
|
+
]
|
|
1131
|
+
: cssPreprocessor === "less"
|
|
1132
|
+
? [
|
|
1133
|
+
"stylelint",
|
|
1134
|
+
"postcss-less",
|
|
1135
|
+
"postcss-html",
|
|
1136
|
+
"stylelint-config-recommended",
|
|
1137
|
+
"stylelint-config-recommended-vue",
|
|
1138
|
+
"stylelint-config-html",
|
|
1139
|
+
"stylelint-config-recess-order",
|
|
1140
|
+
"stylelint-prettier",
|
|
1141
|
+
]
|
|
1142
|
+
: []
|
|
478
1143
|
|
|
479
1144
|
const allPeerDeps = [...baseDeps, ...flatDeps, ...cssDeps]
|
|
480
1145
|
|
|
481
1146
|
// Read peer dependency versions from my-code-style package.json
|
|
482
|
-
const packagePkg = JSON.parse(
|
|
1147
|
+
const packagePkg = JSON.parse(
|
|
1148
|
+
fs.readFileSync(path.resolve(PACKAGE_DIR, "package.json"), "utf8"),
|
|
1149
|
+
)
|
|
483
1150
|
const peerDeps = packagePkg.peerDependencies || {}
|
|
484
1151
|
|
|
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
1152
|
const allDeps = {
|
|
490
1153
|
...(pkg.dependencies || {}),
|
|
491
1154
|
...(pkg.devDependencies || {}),
|
|
@@ -497,9 +1160,10 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
|
|
|
497
1160
|
warn(`以下依赖未安装:${missing.join(", ")}`)
|
|
498
1161
|
console.log("")
|
|
499
1162
|
warn("一键安装命令:")
|
|
500
|
-
console.log(`
|
|
1163
|
+
console.log(` ${addDevCommand(detectPackageManager())} my-code-style \\`)
|
|
501
1164
|
missing.forEach((dep, i) => {
|
|
502
|
-
const version =
|
|
1165
|
+
const version =
|
|
1166
|
+
dep === "eslint" ? (useFlatConfig ? "^9.0.0" : "^8.57.0") : peerDeps[dep] || ""
|
|
503
1167
|
const depWithVersion = version ? `${dep}@${version}` : dep
|
|
504
1168
|
const suffix = i < missing.length - 1 ? " \\" : ""
|
|
505
1169
|
console.log(` ${depWithVersion}${suffix}`)
|
|
@@ -507,4 +1171,7 @@ function checkPeerDeps({ useFlatConfig = false, cssPreprocessor = "scss" } = {})
|
|
|
507
1171
|
}
|
|
508
1172
|
}
|
|
509
1173
|
|
|
510
|
-
|
|
1174
|
+
// Only run when executed directly; requiring this file must be side-effect free.
|
|
1175
|
+
if (require.main === module) {
|
|
1176
|
+
main()
|
|
1177
|
+
}
|