yf-system-cli 1.0.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/README.md +82 -0
- package/bin/cli.js +33 -0
- package/package.json +37 -0
- package/src/commands/init.js +699 -0
- package/src/commands/list.js +27 -0
- package/src/commands/push.js +66 -0
- package/src/commands/remove.js +39 -0
- package/src/config.js +76 -0
- package/src/themes/aaa/package-lock.json +2739 -0
- package/templates/theme/README.md +11 -0
- package/templates/theme/build.js +119 -0
- package/templates/theme/localhost.crt +18 -0
- package/templates/theme/localhost.key +28 -0
- package/templates/theme/package-lock.json +2739 -0
- package/templates/theme/package.json +34 -0
- package/templates/theme/server.js +1723 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
// src/commands/init.js
|
|
2
|
+
const { program } = require("commander");
|
|
3
|
+
const inquirer = require("inquirer");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const ora = require("ora");
|
|
6
|
+
const fs = require("fs-extra");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { getConfig } = require("../config");
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.command("init <project-name>")
|
|
12
|
+
.description("初始化一个新项目")
|
|
13
|
+
.option("-t, --template <template>", "指定模板名称 (application/theme)")
|
|
14
|
+
.option("-f, --force", "强制覆盖已存在的目录")
|
|
15
|
+
.option("--skip-conflict-check", "跳过文件冲突检查")
|
|
16
|
+
.option("--type <type>", "指定项目类型 (application/theme),如果不指定则根据模板类型自动判断")
|
|
17
|
+
.action(async (projectName, options) => {
|
|
18
|
+
try {
|
|
19
|
+
console.log(chalk.cyan("\n🚀 开始创建项目...\n"));
|
|
20
|
+
|
|
21
|
+
// 1. 验证项目名称
|
|
22
|
+
validateProjectName(projectName);
|
|
23
|
+
|
|
24
|
+
// 2. 选择模板
|
|
25
|
+
const templateName = options.template || (await selectTemplate());
|
|
26
|
+
|
|
27
|
+
// 3. 确定项目类型
|
|
28
|
+
const projectType = await determineProjectType(templateName, options.type);
|
|
29
|
+
|
|
30
|
+
// 4. 获取目标目录
|
|
31
|
+
const targetDir = await getTargetDirectory(projectName, projectType);
|
|
32
|
+
|
|
33
|
+
// 5. 检查目录是否存在
|
|
34
|
+
await checkAndHandleExistingDir(targetDir);
|
|
35
|
+
|
|
36
|
+
// 6. 获取模板路径
|
|
37
|
+
const templatePath = getTemplatePath(templateName);
|
|
38
|
+
if (!fs.existsSync(templatePath)) {
|
|
39
|
+
throw new Error(`模板 "${templateName}" 不存在,请检查模板目录`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 7. 创建项目目录
|
|
43
|
+
fs.ensureDirSync(targetDir);
|
|
44
|
+
|
|
45
|
+
// 8. 检查文件冲突
|
|
46
|
+
const conflicts = await checkFileConflicts(templatePath, targetDir);
|
|
47
|
+
if (conflicts.length > 0 && !options.skipConflictCheck) {
|
|
48
|
+
const conflictAction = await handleFileConflicts(conflicts, targetDir, projectName);
|
|
49
|
+
options.conflictAction = conflictAction;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 9. 拷贝模板文件
|
|
53
|
+
await copyTemplateFiles(templatePath, targetDir, projectName, conflicts, options);
|
|
54
|
+
|
|
55
|
+
// 10. 显示成功信息
|
|
56
|
+
showSuccessInfo(targetDir, projectName, templateName, projectType);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(chalk.red("\n❌ 创建项目失败:"), error.message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 验证项目名称
|
|
64
|
+
function validateProjectName(name) {
|
|
65
|
+
if (!name || name.trim() === "") {
|
|
66
|
+
throw new Error("项目名称不能为空");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const invalidChars = /[<>:"/\\|?*\x00-\x1F]/;
|
|
70
|
+
if (invalidChars.test(name)) {
|
|
71
|
+
throw new Error("项目名称包含非法字符");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 确定项目类型
|
|
76
|
+
async function determineProjectType(templateName, specifiedType) {
|
|
77
|
+
// 如果用户指定了类型,使用指定类型
|
|
78
|
+
if (specifiedType) {
|
|
79
|
+
if (!["application", "theme"].includes(specifiedType.toLowerCase())) {
|
|
80
|
+
throw new Error("项目类型必须是 'application' 或 'theme'");
|
|
81
|
+
}
|
|
82
|
+
return specifiedType.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 根据模板名称判断类型
|
|
86
|
+
const templateType = getTemplateType(templateName);
|
|
87
|
+
|
|
88
|
+
// 如果模板类型明确,使用模板类型
|
|
89
|
+
if (templateType) {
|
|
90
|
+
return templateType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 如果无法自动判断,询问用户
|
|
94
|
+
const { projectType } = await inquirer.prompt([
|
|
95
|
+
{
|
|
96
|
+
type: "list",
|
|
97
|
+
name: "projectType",
|
|
98
|
+
message: "请选择项目类型:",
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: "应用", value: "application" },
|
|
101
|
+
{ name: "主题", value: "theme" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
return projectType;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 根据模板名称获取模板类型
|
|
110
|
+
function getTemplateType(templateName) {
|
|
111
|
+
const templateConfig = getTemplateConfig(templateName);
|
|
112
|
+
|
|
113
|
+
// 如果模板配置中有类型定义,使用配置的类型
|
|
114
|
+
if (templateConfig && templateConfig.type) {
|
|
115
|
+
return templateConfig.type;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 根据模板名称关键字判断
|
|
119
|
+
const name = templateName.toLowerCase();
|
|
120
|
+
|
|
121
|
+
if (name.includes("theme") || name.includes("skin") || name.includes("ui") || name.includes("template") || name.includes("样式")) {
|
|
122
|
+
return "theme";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (name.includes("app") || name.includes("application") || name.includes("project") || name.includes("应用")) {
|
|
126
|
+
return "application";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 无法判断时返回 null
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 获取模板配置
|
|
134
|
+
function getTemplateConfig(templateName) {
|
|
135
|
+
const config = getConfig();
|
|
136
|
+
|
|
137
|
+
// 检查配置中的模板设置
|
|
138
|
+
if (config.templates && config.templates[templateName]) {
|
|
139
|
+
return config.templates[templateName];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 检查内置模板的配置文件
|
|
143
|
+
const templatesDir = path.join(__dirname, "../../templates");
|
|
144
|
+
const templatePath = path.join(templatesDir, templateName);
|
|
145
|
+
|
|
146
|
+
if (fs.existsSync(templatePath)) {
|
|
147
|
+
const configFile = path.join(templatePath, "template.config.json");
|
|
148
|
+
if (fs.existsSync(configFile)) {
|
|
149
|
+
try {
|
|
150
|
+
return fs.readJsonSync(configFile);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// 配置文件读取失败,返回空配置
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 获取目标目录
|
|
161
|
+
async function getTargetDirectory(projectName, projectType) {
|
|
162
|
+
const config = getConfig();
|
|
163
|
+
let baseDir = process.cwd();
|
|
164
|
+
|
|
165
|
+
// 检查配置中是否有项目目录设置
|
|
166
|
+
if (config.projectDirs && config.projectDirs[projectType]) {
|
|
167
|
+
baseDir = path.resolve(config.projectDirs[projectType]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 确保基础目录存在
|
|
171
|
+
fs.ensureDirSync(baseDir);
|
|
172
|
+
|
|
173
|
+
// 根据项目类型创建子目录
|
|
174
|
+
let typeDir;
|
|
175
|
+
switch (projectType) {
|
|
176
|
+
case "application":
|
|
177
|
+
typeDir = path.join(baseDir, "src", "applications");
|
|
178
|
+
break;
|
|
179
|
+
case "theme":
|
|
180
|
+
typeDir = path.join(baseDir, "src", "themes");
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
typeDir = baseDir;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 创建类型目录
|
|
187
|
+
fs.ensureDirSync(typeDir);
|
|
188
|
+
|
|
189
|
+
// 检查是否已存在同名项目
|
|
190
|
+
const targetDir = path.join(typeDir, projectName);
|
|
191
|
+
|
|
192
|
+
return targetDir;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 检查并处理已存在的目录
|
|
196
|
+
async function checkAndHandleExistingDir(targetDir) {
|
|
197
|
+
if (fs.existsSync(targetDir)) {
|
|
198
|
+
throw new Error(`目录已存在: ${targetDir}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 选择模板
|
|
203
|
+
async function selectTemplate() {
|
|
204
|
+
// 检查内置模板
|
|
205
|
+
const templatesDir = path.join(__dirname, "../../templates");
|
|
206
|
+
|
|
207
|
+
// 确保模板目录存在
|
|
208
|
+
fs.ensureDirSync(templatesDir);
|
|
209
|
+
|
|
210
|
+
const builtinTemplates = fs.readdirSync(templatesDir).filter((item) => {
|
|
211
|
+
const itemPath = path.join(templatesDir, item);
|
|
212
|
+
return fs.statSync(itemPath).isDirectory();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (builtinTemplates.length === 0) {
|
|
216
|
+
throw new Error("没有找到任何模板,请在 templates/ 目录下创建模板文件夹");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { template } = await inquirer.prompt([
|
|
220
|
+
{
|
|
221
|
+
type: "list",
|
|
222
|
+
name: "template",
|
|
223
|
+
message: "请选择项目模板:",
|
|
224
|
+
choices: builtinTemplates.map((t) => ({
|
|
225
|
+
name: getTemplateDisplayName(t),
|
|
226
|
+
value: t,
|
|
227
|
+
})),
|
|
228
|
+
},
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
return template;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 获取模板显示名称
|
|
235
|
+
function getTemplateDisplayName(template) {
|
|
236
|
+
const config = getTemplateConfig(template);
|
|
237
|
+
|
|
238
|
+
if (config && config.displayName) {
|
|
239
|
+
return `${template} - ${config.displayName}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const names = {
|
|
243
|
+
h5: "H5移动端项目",
|
|
244
|
+
node: "Node.js后端项目",
|
|
245
|
+
vue: "Vue 3前端项目",
|
|
246
|
+
react: "React 18前端项目",
|
|
247
|
+
default: "默认应用模板",
|
|
248
|
+
"admin-theme": "后台管理主题",
|
|
249
|
+
"mobile-theme": "移动端主题",
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const displayName = names[template] || "自定义模板";
|
|
253
|
+
const type = getTemplateType(template);
|
|
254
|
+
const typeLabel = type === "theme" ? "[主题]" : "[应用]";
|
|
255
|
+
|
|
256
|
+
return `${typeLabel} ${template} - ${displayName}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 获取模板路径
|
|
260
|
+
function getTemplatePath(templateName) {
|
|
261
|
+
const templatesDir = path.join(__dirname, "../../templates");
|
|
262
|
+
|
|
263
|
+
// 先检查内置模板
|
|
264
|
+
const builtinPath = path.join(templatesDir, templateName);
|
|
265
|
+
if (fs.existsSync(builtinPath)) {
|
|
266
|
+
return builtinPath;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 检查配置中的自定义模板
|
|
270
|
+
const config = getConfig();
|
|
271
|
+
if (config.templates && config.templates[templateName]) {
|
|
272
|
+
const customPath = config.templates[templateName].path;
|
|
273
|
+
if (customPath && fs.existsSync(customPath)) {
|
|
274
|
+
return customPath;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw new Error(`找不到模板 "${templateName}"`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 检查文件冲突
|
|
282
|
+
async function checkFileConflicts(sourceDir, targetDir) {
|
|
283
|
+
const conflicts = [];
|
|
284
|
+
|
|
285
|
+
// 递归检查所有文件
|
|
286
|
+
const checkDir = async (sourcePath, targetPath) => {
|
|
287
|
+
if (!fs.existsSync(sourcePath)) return;
|
|
288
|
+
|
|
289
|
+
const items = fs.readdirSync(sourcePath);
|
|
290
|
+
|
|
291
|
+
for (const item of items) {
|
|
292
|
+
const sourceItemPath = path.join(sourcePath, item);
|
|
293
|
+
const targetItemPath = path.join(targetPath, item);
|
|
294
|
+
const stats = fs.statSync(sourceItemPath);
|
|
295
|
+
|
|
296
|
+
if (stats.isDirectory()) {
|
|
297
|
+
// 检查是否需要过滤的目录
|
|
298
|
+
const ignoredDirs = ["node_modules", ".git", ".DS_Store", "Thumbs.db"];
|
|
299
|
+
if (ignoredDirs.includes(item)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 递归检查子目录
|
|
304
|
+
await checkDir(sourceItemPath, targetItemPath);
|
|
305
|
+
} else {
|
|
306
|
+
// 检查文件是否已存在
|
|
307
|
+
if (fs.existsSync(targetItemPath)) {
|
|
308
|
+
conflicts.push({
|
|
309
|
+
source: sourceItemPath,
|
|
310
|
+
target: targetItemPath,
|
|
311
|
+
name: item,
|
|
312
|
+
relativePath: path.relative(targetDir, targetItemPath),
|
|
313
|
+
sourceSize: stats.size,
|
|
314
|
+
sourceMtime: stats.mtime,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
await checkDir(sourceDir, targetDir);
|
|
322
|
+
return conflicts;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 处理文件冲突
|
|
326
|
+
async function handleFileConflicts(conflicts, targetDir, projectName) {
|
|
327
|
+
console.log(chalk.yellow("\n⚠️ 检测到以下文件冲突:"));
|
|
328
|
+
|
|
329
|
+
conflicts.forEach((conflict, index) => {
|
|
330
|
+
console.log(chalk.red(` ${index + 1}. ${conflict.relativePath}`));
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const { action } = await inquirer.prompt([
|
|
334
|
+
{
|
|
335
|
+
type: "list",
|
|
336
|
+
name: "action",
|
|
337
|
+
message: `发现 ${conflicts.length} 个文件冲突,请选择处理方式:`,
|
|
338
|
+
choices: [
|
|
339
|
+
{
|
|
340
|
+
name: "跳过冲突文件(保留现有文件)",
|
|
341
|
+
value: "skip",
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: "覆盖所有冲突文件",
|
|
345
|
+
value: "overwrite-all",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "重命名冲突文件",
|
|
349
|
+
value: "rename",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "手动选择每个文件的处理方式",
|
|
353
|
+
value: "manual",
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "取消创建项目",
|
|
357
|
+
value: "cancel",
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
if (action === "cancel") {
|
|
364
|
+
throw new Error("用户取消操作");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return action;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 处理单个文件冲突
|
|
371
|
+
async function handleSingleConflict(conflict, targetDir, projectName) {
|
|
372
|
+
const { action } = await inquirer.prompt([
|
|
373
|
+
{
|
|
374
|
+
type: "list",
|
|
375
|
+
name: "action",
|
|
376
|
+
message: `如何处理文件 "${conflict.relativePath}"?`,
|
|
377
|
+
choices: [
|
|
378
|
+
{ name: "跳过(保留现有文件)", value: "skip" },
|
|
379
|
+
{ name: "覆盖", value: "overwrite" },
|
|
380
|
+
{ name: "重命名", value: "rename" },
|
|
381
|
+
{ name: "比较差异", value: "diff" },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
if (action === "rename") {
|
|
387
|
+
const { newName } = await inquirer.prompt([
|
|
388
|
+
{
|
|
389
|
+
type: "input",
|
|
390
|
+
name: "newName",
|
|
391
|
+
message: `请输入新文件名 (原文件: ${conflict.name}):`,
|
|
392
|
+
default: `${path.basename(conflict.name, path.extname(conflict.name))}_new${path.extname(conflict.name)}`,
|
|
393
|
+
validate: (input) => {
|
|
394
|
+
if (!input || input.trim() === "") {
|
|
395
|
+
return "文件名不能为空";
|
|
396
|
+
}
|
|
397
|
+
const newPath = path.join(path.dirname(conflict.target), input);
|
|
398
|
+
if (fs.existsSync(newPath)) {
|
|
399
|
+
return "新文件名已存在,请重新输入";
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
type: "rename",
|
|
408
|
+
originalPath: conflict.target,
|
|
409
|
+
newPath: path.join(path.dirname(conflict.target), newName),
|
|
410
|
+
};
|
|
411
|
+
} else if (action === "diff") {
|
|
412
|
+
// 显示文件差异(简化版本)
|
|
413
|
+
console.log(chalk.cyan(`\n📄 文件对比: ${conflict.relativePath}`));
|
|
414
|
+
|
|
415
|
+
let sourceContent = "",
|
|
416
|
+
targetContent = "";
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
sourceContent = fs.readFileSync(conflict.source, "utf8");
|
|
420
|
+
targetContent = fs.readFileSync(conflict.target, "utf8");
|
|
421
|
+
|
|
422
|
+
console.log(chalk.green("模板文件内容:"));
|
|
423
|
+
console.log(sourceContent.substring(0, 200) + (sourceContent.length > 200 ? "..." : ""));
|
|
424
|
+
console.log(chalk.yellow("\n现有文件内容:"));
|
|
425
|
+
console.log(targetContent.substring(0, 200) + (targetContent.length > 200 ? "..." : ""));
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.log(chalk.red("无法读取文件内容"));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 重新询问处理方式
|
|
431
|
+
return handleSingleConflict(conflict, targetDir, projectName);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { type: action };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 拷贝模板文件(增强版本)
|
|
438
|
+
async function copyTemplateFiles(sourceDir, targetDir, projectName, conflicts = [], options = {}) {
|
|
439
|
+
const spinner = ora("正在拷贝模板文件...").start();
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
// 记录要跳过的文件和重命名的文件
|
|
443
|
+
const skipFiles = new Set();
|
|
444
|
+
const renameMap = new Map();
|
|
445
|
+
|
|
446
|
+
// 如果是手动处理模式
|
|
447
|
+
if (options.conflictAction === "manual") {
|
|
448
|
+
for (const conflict of conflicts) {
|
|
449
|
+
const result = await handleSingleConflict(conflict, targetDir, projectName);
|
|
450
|
+
|
|
451
|
+
if (result.type === "skip") {
|
|
452
|
+
skipFiles.add(conflict.target);
|
|
453
|
+
} else if (result.type === "rename") {
|
|
454
|
+
skipFiles.add(conflict.target); // 原文件跳过
|
|
455
|
+
renameMap.set(conflict.source, result.newPath);
|
|
456
|
+
}
|
|
457
|
+
// overwrite 不需要特别处理,会正常覆盖
|
|
458
|
+
}
|
|
459
|
+
} else if (options.conflictAction === "skip") {
|
|
460
|
+
// 跳过所有冲突文件
|
|
461
|
+
conflicts.forEach((conflict) => {
|
|
462
|
+
skipFiles.add(conflict.target);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// overwrite-all 不需要特殊处理
|
|
466
|
+
|
|
467
|
+
// 递归拷贝文件
|
|
468
|
+
await copyWithConflicts(sourceDir, targetDir, skipFiles, renameMap);
|
|
469
|
+
|
|
470
|
+
spinner.succeed(chalk.green("✓ 模板文件拷贝完成"));
|
|
471
|
+
|
|
472
|
+
// 替换文件中的占位符(排除跳过的文件)
|
|
473
|
+
await replacePlaceholders(targetDir, projectName, skipFiles);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
spinner.fail("拷贝模板文件失败");
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 带冲突处理的拷贝函数
|
|
481
|
+
async function copyWithConflicts(sourceDir, targetDir, skipFiles, renameMap) {
|
|
482
|
+
const items = fs.readdirSync(sourceDir);
|
|
483
|
+
|
|
484
|
+
for (const item of items) {
|
|
485
|
+
const sourcePath = path.join(sourceDir, item);
|
|
486
|
+
const targetPath = path.join(targetDir, item);
|
|
487
|
+
const stats = fs.statSync(sourcePath);
|
|
488
|
+
|
|
489
|
+
// 跳过不需要的文件
|
|
490
|
+
if (shouldSkipFile(item)) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 检查是否要重命名
|
|
495
|
+
const actualTargetPath = renameMap.get(sourcePath) || targetPath;
|
|
496
|
+
|
|
497
|
+
if (stats.isDirectory()) {
|
|
498
|
+
// 创建目录并递归拷贝
|
|
499
|
+
fs.ensureDirSync(actualTargetPath);
|
|
500
|
+
await copyWithConflicts(sourcePath, actualTargetPath, skipFiles, renameMap);
|
|
501
|
+
} else {
|
|
502
|
+
// 检查是否要跳过此文件
|
|
503
|
+
if (skipFiles.has(targetPath)) {
|
|
504
|
+
console.log(chalk.gray(` 跳过: ${path.relative(targetDir, targetPath)}`));
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 检查目标目录是否存在同名文件(防止冲突)
|
|
509
|
+
const targetDirPath = path.dirname(actualTargetPath);
|
|
510
|
+
const targetFileName = path.basename(actualTargetPath);
|
|
511
|
+
|
|
512
|
+
// 如果目标文件已存在且不在skip列表中,生成新文件名
|
|
513
|
+
if (fs.existsSync(actualTargetPath) && !skipFiles.has(actualTargetPath)) {
|
|
514
|
+
let counter = 1;
|
|
515
|
+
let newFileName, newFilePath;
|
|
516
|
+
|
|
517
|
+
do {
|
|
518
|
+
const ext = path.extname(targetFileName);
|
|
519
|
+
const name = path.basename(targetFileName, ext);
|
|
520
|
+
newFileName = `${name}_${counter}${ext}`;
|
|
521
|
+
newFilePath = path.join(targetDirPath, newFileName);
|
|
522
|
+
counter++;
|
|
523
|
+
} while (fs.existsSync(newFilePath));
|
|
524
|
+
|
|
525
|
+
console.log(chalk.yellow(` 重命名冲突文件: ${targetFileName} -> ${newFileName}`));
|
|
526
|
+
fs.copySync(sourcePath, newFilePath);
|
|
527
|
+
} else {
|
|
528
|
+
// 正常拷贝
|
|
529
|
+
fs.copySync(sourcePath, actualTargetPath);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 判断是否要跳过文件
|
|
536
|
+
function shouldSkipFile(filename) {
|
|
537
|
+
const ignoredItems = ["node_modules", ".git", ".DS_Store", "Thumbs.db", ".vscode", ".idea", "dist", "build", ".env.local", ".npmrc", ".yarnrc"];
|
|
538
|
+
|
|
539
|
+
if (ignoredItems.includes(filename)) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 跳过临时文件
|
|
544
|
+
if (filename.endsWith(".log") || filename.endsWith(".tmp") || filename.startsWith("~$") || filename === ".tmp") {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// 替换文件中的占位符(增强版本)
|
|
552
|
+
async function replacePlaceholders(projectDir, projectName, skipFiles = new Set()) {
|
|
553
|
+
// 递归处理目录
|
|
554
|
+
const processDir = async (dir) => {
|
|
555
|
+
if (!fs.existsSync(dir)) return;
|
|
556
|
+
|
|
557
|
+
const items = fs.readdirSync(dir);
|
|
558
|
+
|
|
559
|
+
for (const item of items) {
|
|
560
|
+
const itemPath = path.join(dir, item);
|
|
561
|
+
|
|
562
|
+
// 跳过冲突文件
|
|
563
|
+
if (skipFiles.has(itemPath)) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const stats = fs.statSync(itemPath);
|
|
568
|
+
|
|
569
|
+
if (stats.isDirectory()) {
|
|
570
|
+
// 跳过不需要的目录
|
|
571
|
+
if (["node_modules", ".git", "dist", "build"].includes(item)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
await processDir(itemPath);
|
|
575
|
+
} else if (stats.isFile()) {
|
|
576
|
+
await replaceInFile(itemPath, projectName);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
await processDir(projectDir);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 在单个文件中替换占位符
|
|
585
|
+
async function replaceInFile(filePath, projectName) {
|
|
586
|
+
// 只处理文本文件
|
|
587
|
+
const textExtensions = [".js", ".ts", ".jsx", ".tsx", ".vue", ".md", ".txt", ".json", ".yml", ".yaml", ".xml", ".html", ".htm", ".css", ".scss", ".less", ".php", ".py", ".java", ".c", ".cpp", ".go", ".rb", ".rs"];
|
|
588
|
+
|
|
589
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
590
|
+
if (!textExtensions.includes(ext)) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
596
|
+
let modified = false;
|
|
597
|
+
|
|
598
|
+
// 常见的占位符映射
|
|
599
|
+
const placeholders = {
|
|
600
|
+
"{{project-name}}": projectName,
|
|
601
|
+
"{{project_name}}": projectName.replace(/-/g, "_"),
|
|
602
|
+
"{{projectName}}": projectName.replace(/-(\w)/g, (_, c) => c.toUpperCase()),
|
|
603
|
+
"{{ProjectName}}": projectName.replace(/-(\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase()),
|
|
604
|
+
"{{PROJECT_NAME}}": projectName.toUpperCase().replace(/-/g, "_"),
|
|
605
|
+
"{{name}}": projectName,
|
|
606
|
+
"{{NAME}}": projectName.toUpperCase(),
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// 替换所有占位符
|
|
610
|
+
for (const [placeholder, value] of Object.entries(placeholders)) {
|
|
611
|
+
if (content.includes(placeholder)) {
|
|
612
|
+
content = content.replace(new RegExp(placeholder, "g"), value);
|
|
613
|
+
modified = true;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 特别处理 package.json
|
|
618
|
+
if (path.basename(filePath) === "package.json") {
|
|
619
|
+
try {
|
|
620
|
+
const packageJson = JSON.parse(content);
|
|
621
|
+
let pkgModified = false;
|
|
622
|
+
|
|
623
|
+
// 替换各种可能的占位符格式
|
|
624
|
+
const pkgFields = ["name", "description", "version", "author"];
|
|
625
|
+
for (const field of pkgFields) {
|
|
626
|
+
if (packageJson[field] && typeof packageJson[field] === "string") {
|
|
627
|
+
for (const [placeholder, value] of Object.entries(placeholders)) {
|
|
628
|
+
if (packageJson[field].includes(placeholder)) {
|
|
629
|
+
packageJson[field] = packageJson[field].replace(new RegExp(placeholder, "g"), field === "name" ? projectName : value);
|
|
630
|
+
pkgModified = true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (pkgModified) {
|
|
637
|
+
content = JSON.stringify(packageJson, null, 2);
|
|
638
|
+
modified = true;
|
|
639
|
+
}
|
|
640
|
+
} catch (e) {
|
|
641
|
+
// JSON 解析失败,跳过
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (modified) {
|
|
646
|
+
fs.writeFileSync(filePath, content);
|
|
647
|
+
}
|
|
648
|
+
} catch (error) {
|
|
649
|
+
// 文件读取失败,跳过
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 显示成功信息
|
|
654
|
+
function showSuccessInfo(targetDir, projectName, templateName, projectType) {
|
|
655
|
+
console.log(chalk.green("\n🎉 项目创建成功!"));
|
|
656
|
+
console.log(chalk.cyan("══════════════════════════════════════\n"));
|
|
657
|
+
|
|
658
|
+
console.log(chalk.white.bold("📁 项目信息:"));
|
|
659
|
+
console.log(chalk.cyan(` 项目名称: ${projectName}`));
|
|
660
|
+
console.log(chalk.cyan(` 项目路径: ${targetDir}`));
|
|
661
|
+
console.log(chalk.cyan(` 项目类型: ${projectType === "application" ? "应用程序" : "主题"}`));
|
|
662
|
+
console.log(chalk.cyan(` 使用模板: ${templateName}`));
|
|
663
|
+
|
|
664
|
+
const relativePath = path.relative(process.cwd(), targetDir);
|
|
665
|
+
|
|
666
|
+
console.log(chalk.white.bold("\n🚀 下一步操作:"));
|
|
667
|
+
console.log(chalk.cyan(` cd ${relativePath}`));
|
|
668
|
+
|
|
669
|
+
// 检查是否有 package.json 来推荐安装依赖
|
|
670
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
671
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
672
|
+
console.log(chalk.cyan(" npm install # 安装依赖"));
|
|
673
|
+
|
|
674
|
+
// 根据常见的启动脚本推荐命令
|
|
675
|
+
try {
|
|
676
|
+
const packageJson = fs.readJsonSync(packageJsonPath);
|
|
677
|
+
if (packageJson.scripts) {
|
|
678
|
+
if (packageJson.scripts.dev) {
|
|
679
|
+
console.log(chalk.cyan(` npm run dev # 启动开发服务器`));
|
|
680
|
+
} else if (packageJson.scripts.start) {
|
|
681
|
+
console.log(chalk.cyan(` npm start # 启动项目`));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
} catch (error) {
|
|
685
|
+
// 解析失败,使用默认推荐
|
|
686
|
+
console.log(chalk.cyan(` npm start # 启动项目`));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
console.log(chalk.cyan("\n📚 查看文档:"));
|
|
691
|
+
console.log(chalk.cyan(` 查看 ${projectName}/README.md 获取详细说明`));
|
|
692
|
+
|
|
693
|
+
console.log(chalk.yellow("\n💡 提示:"));
|
|
694
|
+
console.log(chalk.cyan(" 如果存在文件冲突,请检查是否有文件被重命名"));
|
|
695
|
+
|
|
696
|
+
console.log(chalk.green("\n✨ 开始您的开发之旅吧!\n"));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
module.exports = { program };
|