ylyx-cli 1.0.14 → 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 +4 -0
- package/lib/deploy.js +82 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,10 @@ prod 输出目录规则:
|
|
|
163
163
|
|
|
164
164
|
`localDir` 默认推导优先级:`deploy.localDir` → `.env.production` 的 `VUE_APP_PUBLIC_URL` → `.ylyxrc.json` 的 `buildDir`。
|
|
165
165
|
|
|
166
|
+
### 上传日志
|
|
167
|
+
|
|
168
|
+
每次执行 `deploy` 会在本地生成一份日志,默认在 `./.ylyx-deploy/logs/`,包含上传/压缩过程与错误信息,便于追溯。
|
|
169
|
+
|
|
166
170
|
### 命令
|
|
167
171
|
|
|
168
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];
|
|
@@ -293,14 +342,21 @@ async function deploy(overrides = {}) {
|
|
|
293
342
|
const remoteFolderPath = cfg.remoteDir;
|
|
294
343
|
|
|
295
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);
|
|
296
350
|
|
|
297
351
|
if (!fs.existsSync(localFolderPath)) {
|
|
352
|
+
logger.logOp('ERROR', localFolderPath, 'localDir_not_exists');
|
|
353
|
+
await logger.close();
|
|
298
354
|
throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
|
|
299
355
|
}
|
|
300
356
|
|
|
301
357
|
// 正式服:仅压缩
|
|
302
358
|
if (env === 'prod') {
|
|
303
|
-
const buildDirName =
|
|
359
|
+
const buildDirName = buildDirNameForLog;
|
|
304
360
|
const ts = formatTimestampCompact(new Date());
|
|
305
361
|
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
306
362
|
// 优化命名:更可读、更易分割(buildDir-时间-哈希)
|
|
@@ -308,15 +364,21 @@ async function deploy(overrides = {}) {
|
|
|
308
364
|
fs.ensureDirSync(cfg.zipOutDir);
|
|
309
365
|
const zipName = `${zipBaseName}.zip`;
|
|
310
366
|
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
367
|
+
logger.logOp('ZIP_START', zipPath);
|
|
311
368
|
|
|
312
369
|
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
313
370
|
try {
|
|
314
371
|
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
315
372
|
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
373
|
+
logger.logOp('ZIP_DONE', zipPath);
|
|
316
374
|
} catch (e) {
|
|
317
375
|
zipSpinner.fail('压缩失败');
|
|
376
|
+
logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
|
|
377
|
+
await logger.close();
|
|
318
378
|
throw e;
|
|
319
379
|
}
|
|
380
|
+
logger.logOp('DEPLOY_END', '', 'prod');
|
|
381
|
+
await logger.close();
|
|
320
382
|
return;
|
|
321
383
|
}
|
|
322
384
|
|
|
@@ -325,6 +387,8 @@ async function deploy(overrides = {}) {
|
|
|
325
387
|
if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
|
|
326
388
|
if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
|
|
327
389
|
if (!remoteFolderPath) {
|
|
390
|
+
logger.logOp('ERROR', '', 'missing_remoteDir');
|
|
391
|
+
await logger.close();
|
|
328
392
|
throw new Error('缺少 deploy.remoteDir(测试服上传需要):请在 .ylyxrc.json 的 deploy.remoteDir 配置远端部署目录');
|
|
329
393
|
}
|
|
330
394
|
|
|
@@ -342,6 +406,7 @@ async function deploy(overrides = {}) {
|
|
|
342
406
|
const sftp = new Client();
|
|
343
407
|
try {
|
|
344
408
|
spinner = ora('连接服务器...').start();
|
|
409
|
+
logger.logOp('SFTP_CONNECT', cfg.host, `port=${cfg.port} username=${cfg.username}`);
|
|
345
410
|
await sftp.connect({
|
|
346
411
|
host: cfg.host,
|
|
347
412
|
port: cfg.port,
|
|
@@ -350,15 +415,20 @@ async function deploy(overrides = {}) {
|
|
|
350
415
|
});
|
|
351
416
|
|
|
352
417
|
spinner.text = '服务器连接成功,准备清理远端目录...';
|
|
418
|
+
logger.logOp('REMOTE_DELETE_START', remoteFolderPath);
|
|
353
419
|
try {
|
|
354
420
|
await sftp.rmdir(remoteFolderPath, true);
|
|
421
|
+
logger.logOp('REMOTE_DELETE_DONE', remoteFolderPath);
|
|
355
422
|
} catch (e) {
|
|
356
423
|
// ignore
|
|
424
|
+
logger.logOp('REMOTE_DELETE_SKIP', remoteFolderPath, e && e.message ? e.message : 'ignored');
|
|
357
425
|
}
|
|
358
426
|
await sftp.mkdir(remoteFolderPath, true);
|
|
427
|
+
logger.logOp('REMOTE_MKDIR', remoteFolderPath);
|
|
359
428
|
|
|
360
429
|
const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
|
|
361
430
|
spinner.text = '开始上传目录...';
|
|
431
|
+
logger.logOp('UPLOAD_START', remoteDirPosix, `from=${localFolderPath}`);
|
|
362
432
|
const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
|
|
363
433
|
sftp,
|
|
364
434
|
localDir: localFolderPath,
|
|
@@ -370,8 +440,11 @@ async function deploy(overrides = {}) {
|
|
|
370
440
|
},
|
|
371
441
|
});
|
|
372
442
|
spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
|
|
443
|
+
logger.logOp('UPLOAD_DONE', remoteDirPosix, `files=${fileCount} bytes=${uploadedBytes}/${totalBytes}`);
|
|
373
444
|
} catch (err) {
|
|
374
445
|
if (spinner) spinner.fail('部署失败');
|
|
446
|
+
logger.logOp('ERROR', '', err && err.message ? err.message : String(err));
|
|
447
|
+
await logger.close();
|
|
375
448
|
throw err;
|
|
376
449
|
} finally {
|
|
377
450
|
try {
|
|
@@ -382,23 +455,30 @@ async function deploy(overrides = {}) {
|
|
|
382
455
|
|
|
383
456
|
// 测试服可选:部署后压缩(延续之前的可选开关)
|
|
384
457
|
if (cfg.zipAfter) {
|
|
385
|
-
const buildDirName =
|
|
458
|
+
const buildDirName = buildDirNameForLog;
|
|
386
459
|
const ts = formatTimestampCompact(new Date());
|
|
387
460
|
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
388
461
|
const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
|
|
389
462
|
fs.ensureDirSync(cfg.zipOutDir);
|
|
390
463
|
const zipName = `${zipBaseName}.zip`;
|
|
391
464
|
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
465
|
+
logger.logOp('ZIP_START', zipPath);
|
|
392
466
|
|
|
393
467
|
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
394
468
|
try {
|
|
395
469
|
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
396
470
|
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
471
|
+
logger.logOp('ZIP_DONE', zipPath);
|
|
397
472
|
} catch (e) {
|
|
398
473
|
zipSpinner.fail('压缩失败');
|
|
474
|
+
logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
|
|
475
|
+
await logger.close();
|
|
399
476
|
throw e;
|
|
400
477
|
}
|
|
401
478
|
}
|
|
479
|
+
|
|
480
|
+
logger.logOp('DEPLOY_END', '', 'test');
|
|
481
|
+
await logger.close();
|
|
402
482
|
}
|
|
403
483
|
|
|
404
484
|
module.exports = {
|