ylyx-cli 1.0.13 → 1.0.15
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 +6 -0
- package/lib/deploy.js +96 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
公司内部使用的项目脚手架工具:支持交互式/命令式生成项目与代码模板,并提供 `.ylyxrc.json` 项目配置、环境配置切换(`isDev`)、以及一键部署(测试服上传/正式服打包压缩)等能力。
|
|
4
4
|
|
|
5
|
+
> 说明:当前部署/配置切换能力**暂时只针对 Vue2 项目约定**(如 `.env.production` 的 `VUE_APP_PUBLIC_URL` 等)。其它技术栈/项目结构可能需要你根据实际情况调整 `.ylyxrc.json` 与相关路径配置。
|
|
6
|
+
|
|
5
7
|
## 功能特性
|
|
6
8
|
|
|
7
9
|
- 🚀 **快速生成项目代码**:内置多种模板,一键生成项目/组件/模块代码
|
|
@@ -161,6 +163,10 @@ prod 输出目录规则:
|
|
|
161
163
|
|
|
162
164
|
`localDir` 默认推导优先级:`deploy.localDir` → `.env.production` 的 `VUE_APP_PUBLIC_URL` → `.ylyxrc.json` 的 `buildDir`。
|
|
163
165
|
|
|
166
|
+
### 上传日志
|
|
167
|
+
|
|
168
|
+
每次执行 `deploy` 会在本地生成一份日志,默认在 `./.ylyx-deploy/logs/`,包含上传/压缩过程与错误信息,便于追溯。
|
|
169
|
+
|
|
164
170
|
### 命令
|
|
165
171
|
|
|
166
172
|
```bash
|
package/lib/deploy.js
CHANGED
|
@@ -18,6 +18,17 @@ function formatTimestampCompact(date = new Date()) {
|
|
|
18
18
|
return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function formatTimestampForLog(date = new Date()) {
|
|
22
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
23
|
+
const yyyy = date.getFullYear();
|
|
24
|
+
const MM = pad2(date.getMonth() + 1);
|
|
25
|
+
const dd = pad2(date.getDate());
|
|
26
|
+
const HH = pad2(date.getHours());
|
|
27
|
+
const mm = pad2(date.getMinutes());
|
|
28
|
+
const ss = pad2(date.getSeconds());
|
|
29
|
+
return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
function formatBytes(bytes) {
|
|
22
33
|
const n = Number(bytes) || 0;
|
|
23
34
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
@@ -31,6 +42,44 @@ function formatBytes(bytes) {
|
|
|
31
42
|
return `${v.toFixed(fixed)}${units[i]}`;
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
function createDeployLogger({ env, buildDirName, outDir }) {
|
|
46
|
+
const logDir = path.join(outDir || path.join(process.cwd(), '.ylyx-deploy'), 'logs');
|
|
47
|
+
fs.ensureDirSync(logDir);
|
|
48
|
+
const fileName = `deploy-${env || 'unknown'}-${buildDirName || 'build'}-${formatTimestampCompact(
|
|
49
|
+
new Date()
|
|
50
|
+
)}.log`;
|
|
51
|
+
const logPath = path.join(logDir, fileName);
|
|
52
|
+
const stream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
53
|
+
|
|
54
|
+
const log = (msg) => {
|
|
55
|
+
const line = `[${formatTimestampForLog(new Date())}] ${msg}\n`;
|
|
56
|
+
try {
|
|
57
|
+
stream.write(line);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 只记录“操作/目录/时间”的精简日志
|
|
64
|
+
const logOp = (op, target, extra) => {
|
|
65
|
+
const parts = [`op=${op}`];
|
|
66
|
+
if (target) parts.push(`target=${target}`);
|
|
67
|
+
if (extra) parts.push(`extra=${extra}`);
|
|
68
|
+
log(parts.join(' '));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const close = () =>
|
|
72
|
+
new Promise((resolve) => {
|
|
73
|
+
try {
|
|
74
|
+
stream.end(resolve);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
resolve();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { logPath, log, logOp, close };
|
|
81
|
+
}
|
|
82
|
+
|
|
34
83
|
function listLocalFilesWithSize(rootDir) {
|
|
35
84
|
const out = [];
|
|
36
85
|
const stack = [rootDir];
|
|
@@ -254,8 +303,16 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
254
303
|
const inferredLocalDir =
|
|
255
304
|
deploy.localDir ||
|
|
256
305
|
inferredFromEnvProd ||
|
|
257
|
-
(projectConfig && projectConfig.buildDir ? path.resolve(process.cwd(), projectConfig.buildDir) : null)
|
|
258
|
-
|
|
306
|
+
(projectConfig && projectConfig.buildDir ? path.resolve(process.cwd(), projectConfig.buildDir) : null);
|
|
307
|
+
|
|
308
|
+
if (!inferredLocalDir) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
'无法确定 deploy.localDir(本地打包目录)。请设置以下任意一种:\n' +
|
|
311
|
+
'- 在 .ylyxrc.json 配置 deploy.localDir\n' +
|
|
312
|
+
'- 在项目 .env.production 配置 VUE_APP_PUBLIC_URL(相对路径,如 /dist/app/)\n' +
|
|
313
|
+
'- 在 .ylyxrc.json 配置 buildDir(可相对/绝对路径)'
|
|
314
|
+
);
|
|
315
|
+
}
|
|
259
316
|
|
|
260
317
|
const cfg = {
|
|
261
318
|
env: overrides.env ?? deploy.env,
|
|
@@ -265,7 +322,7 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
265
322
|
username: overrides.username ?? deploy.username,
|
|
266
323
|
password: overrides.password ?? deploy.password,
|
|
267
324
|
localDir: overrides.localDir ?? inferredLocalDir,
|
|
268
|
-
remoteDir: overrides.remoteDir ?? deploy.remoteDir
|
|
325
|
+
remoteDir: overrides.remoteDir ?? deploy.remoteDir,
|
|
269
326
|
zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
|
|
270
327
|
zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
|
|
271
328
|
};
|
|
@@ -285,14 +342,21 @@ async function deploy(overrides = {}) {
|
|
|
285
342
|
const remoteFolderPath = cfg.remoteDir;
|
|
286
343
|
|
|
287
344
|
const env = await chooseDeployEnv(cfg.env);
|
|
345
|
+
const buildDirNameForLog = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
|
|
346
|
+
const logger = createDeployLogger({ env, buildDirName: buildDirNameForLog, outDir: cfg.zipOutDir });
|
|
347
|
+
logger.logOp('DEPLOY_START', '', `env=${env}`);
|
|
348
|
+
logger.logOp('LOCAL_DIR', localFolderPath);
|
|
349
|
+
if (remoteFolderPath) logger.logOp('REMOTE_DIR', remoteFolderPath);
|
|
288
350
|
|
|
289
351
|
if (!fs.existsSync(localFolderPath)) {
|
|
352
|
+
logger.logOp('ERROR', localFolderPath, 'localDir_not_exists');
|
|
353
|
+
await logger.close();
|
|
290
354
|
throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
|
|
291
355
|
}
|
|
292
356
|
|
|
293
357
|
// 正式服:仅压缩
|
|
294
358
|
if (env === 'prod') {
|
|
295
|
-
const buildDirName =
|
|
359
|
+
const buildDirName = buildDirNameForLog;
|
|
296
360
|
const ts = formatTimestampCompact(new Date());
|
|
297
361
|
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
298
362
|
// 优化命名:更可读、更易分割(buildDir-时间-哈希)
|
|
@@ -300,15 +364,21 @@ async function deploy(overrides = {}) {
|
|
|
300
364
|
fs.ensureDirSync(cfg.zipOutDir);
|
|
301
365
|
const zipName = `${zipBaseName}.zip`;
|
|
302
366
|
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
367
|
+
logger.logOp('ZIP_START', zipPath);
|
|
303
368
|
|
|
304
369
|
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
305
370
|
try {
|
|
306
371
|
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
307
372
|
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
373
|
+
logger.logOp('ZIP_DONE', zipPath);
|
|
308
374
|
} catch (e) {
|
|
309
375
|
zipSpinner.fail('压缩失败');
|
|
376
|
+
logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
|
|
377
|
+
await logger.close();
|
|
310
378
|
throw e;
|
|
311
379
|
}
|
|
380
|
+
logger.logOp('DEPLOY_END', '', 'prod');
|
|
381
|
+
await logger.close();
|
|
312
382
|
return;
|
|
313
383
|
}
|
|
314
384
|
|
|
@@ -316,7 +386,11 @@ async function deploy(overrides = {}) {
|
|
|
316
386
|
if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
|
|
317
387
|
if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
|
|
318
388
|
if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
|
|
319
|
-
if (!remoteFolderPath)
|
|
389
|
+
if (!remoteFolderPath) {
|
|
390
|
+
logger.logOp('ERROR', '', 'missing_remoteDir');
|
|
391
|
+
await logger.close();
|
|
392
|
+
throw new Error('缺少 deploy.remoteDir(测试服上传需要):请在 .ylyxrc.json 的 deploy.remoteDir 配置远端部署目录');
|
|
393
|
+
}
|
|
320
394
|
|
|
321
395
|
let Client;
|
|
322
396
|
try {
|
|
@@ -332,6 +406,7 @@ async function deploy(overrides = {}) {
|
|
|
332
406
|
const sftp = new Client();
|
|
333
407
|
try {
|
|
334
408
|
spinner = ora('连接服务器...').start();
|
|
409
|
+
logger.logOp('SFTP_CONNECT', cfg.host, `port=${cfg.port} username=${cfg.username}`);
|
|
335
410
|
await sftp.connect({
|
|
336
411
|
host: cfg.host,
|
|
337
412
|
port: cfg.port,
|
|
@@ -340,15 +415,20 @@ async function deploy(overrides = {}) {
|
|
|
340
415
|
});
|
|
341
416
|
|
|
342
417
|
spinner.text = '服务器连接成功,准备清理远端目录...';
|
|
418
|
+
logger.logOp('REMOTE_DELETE_START', remoteFolderPath);
|
|
343
419
|
try {
|
|
344
420
|
await sftp.rmdir(remoteFolderPath, true);
|
|
421
|
+
logger.logOp('REMOTE_DELETE_DONE', remoteFolderPath);
|
|
345
422
|
} catch (e) {
|
|
346
423
|
// ignore
|
|
424
|
+
logger.logOp('REMOTE_DELETE_SKIP', remoteFolderPath, e && e.message ? e.message : 'ignored');
|
|
347
425
|
}
|
|
348
426
|
await sftp.mkdir(remoteFolderPath, true);
|
|
427
|
+
logger.logOp('REMOTE_MKDIR', remoteFolderPath);
|
|
349
428
|
|
|
350
429
|
const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
|
|
351
430
|
spinner.text = '开始上传目录...';
|
|
431
|
+
logger.logOp('UPLOAD_START', remoteDirPosix, `from=${localFolderPath}`);
|
|
352
432
|
const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
|
|
353
433
|
sftp,
|
|
354
434
|
localDir: localFolderPath,
|
|
@@ -360,8 +440,11 @@ async function deploy(overrides = {}) {
|
|
|
360
440
|
},
|
|
361
441
|
});
|
|
362
442
|
spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
|
|
443
|
+
logger.logOp('UPLOAD_DONE', remoteDirPosix, `files=${fileCount} bytes=${uploadedBytes}/${totalBytes}`);
|
|
363
444
|
} catch (err) {
|
|
364
445
|
if (spinner) spinner.fail('部署失败');
|
|
446
|
+
logger.logOp('ERROR', '', err && err.message ? err.message : String(err));
|
|
447
|
+
await logger.close();
|
|
365
448
|
throw err;
|
|
366
449
|
} finally {
|
|
367
450
|
try {
|
|
@@ -372,23 +455,30 @@ async function deploy(overrides = {}) {
|
|
|
372
455
|
|
|
373
456
|
// 测试服可选:部署后压缩(延续之前的可选开关)
|
|
374
457
|
if (cfg.zipAfter) {
|
|
375
|
-
const buildDirName =
|
|
458
|
+
const buildDirName = buildDirNameForLog;
|
|
376
459
|
const ts = formatTimestampCompact(new Date());
|
|
377
460
|
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
378
461
|
const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
|
|
379
462
|
fs.ensureDirSync(cfg.zipOutDir);
|
|
380
463
|
const zipName = `${zipBaseName}.zip`;
|
|
381
464
|
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
465
|
+
logger.logOp('ZIP_START', zipPath);
|
|
382
466
|
|
|
383
467
|
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
384
468
|
try {
|
|
385
469
|
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
386
470
|
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
471
|
+
logger.logOp('ZIP_DONE', zipPath);
|
|
387
472
|
} catch (e) {
|
|
388
473
|
zipSpinner.fail('压缩失败');
|
|
474
|
+
logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
|
|
475
|
+
await logger.close();
|
|
389
476
|
throw e;
|
|
390
477
|
}
|
|
391
478
|
}
|
|
479
|
+
|
|
480
|
+
logger.logOp('DEPLOY_END', '', 'test');
|
|
481
|
+
await logger.close();
|
|
392
482
|
}
|
|
393
483
|
|
|
394
484
|
module.exports = {
|