ylyx-cli 1.0.5 → 1.0.7
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/ylyx.js +4 -0
- package/lib/deploy.js +128 -22
- package/package.json +2 -2
package/bin/ylyx.js
CHANGED
|
@@ -114,6 +114,8 @@ program
|
|
|
114
114
|
program
|
|
115
115
|
.command('deploy')
|
|
116
116
|
.description('部署:递归上传本地目录到远端目录(读取当前项目 .ylyxrc.json 的 deploy 字段)')
|
|
117
|
+
.option('--env <env>', '部署环境:test|prod(不传会交互选择)')
|
|
118
|
+
.option('--build-dir <name>', '压缩文件名使用的 buildDir(默认取 .ylyxrc.json 的 buildDir)')
|
|
117
119
|
.option('--host <host>', '覆盖 deploy.host')
|
|
118
120
|
.option('--port <port>', '覆盖 deploy.port')
|
|
119
121
|
.option('-u, --username <username>', '覆盖 deploy.username')
|
|
@@ -125,6 +127,8 @@ program
|
|
|
125
127
|
.action(async (options) => {
|
|
126
128
|
try {
|
|
127
129
|
await deploy({
|
|
130
|
+
env: options.env,
|
|
131
|
+
buildDir: options.buildDir,
|
|
128
132
|
host: options.host,
|
|
129
133
|
port: options.port,
|
|
130
134
|
username: options.username,
|
package/lib/deploy.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const ora = require('ora');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const archiver = require('archiver');
|
|
4
6
|
const { readProjectConfig } = require('./utils');
|
|
5
7
|
|
|
6
8
|
let spinner = null; // 加载实例
|
|
7
9
|
|
|
8
|
-
function
|
|
10
|
+
function formatTimestampCompact(date = new Date()) {
|
|
9
11
|
const pad2 = (n) => String(n).padStart(2, '0');
|
|
10
12
|
const yyyy = date.getFullYear();
|
|
11
13
|
const MM = pad2(date.getMonth() + 1);
|
|
@@ -13,8 +15,7 @@ function formatTimestampForZipName(date = new Date()) {
|
|
|
13
15
|
const HH = pad2(date.getHours());
|
|
14
16
|
const mm = pad2(date.getMinutes());
|
|
15
17
|
const ss = pad2(date.getSeconds());
|
|
16
|
-
|
|
17
|
-
return `${yyyy}-${MM}-${dd}_${HH}-${mm}-${ss}`;
|
|
18
|
+
return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function formatBytes(bytes) {
|
|
@@ -45,6 +46,7 @@ function listLocalFilesWithSize(rootDir) {
|
|
|
45
46
|
localPath: p,
|
|
46
47
|
relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
|
|
47
48
|
size: st.size,
|
|
49
|
+
mtimeMs: st.mtimeMs,
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -53,6 +55,80 @@ function listLocalFilesWithSize(rootDir) {
|
|
|
53
55
|
return out;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
function sanitizeBuildDirName(name) {
|
|
59
|
+
// 用户要求:项目名称来源于 buildDir 字段;同时确保文件名安全
|
|
60
|
+
return String(name || 'build')
|
|
61
|
+
.replace(/[\\/]/g, '')
|
|
62
|
+
.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
63
|
+
.slice(0, 80);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
|
|
67
|
+
fs.ensureDirSync(path.dirname(zipPath));
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
const output = fs.createWriteStream(zipPath);
|
|
70
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
71
|
+
|
|
72
|
+
output.on('close', resolve);
|
|
73
|
+
output.on('error', reject);
|
|
74
|
+
archive.on('warning', (err) => {
|
|
75
|
+
if (err && err.code === 'ENOENT') return;
|
|
76
|
+
reject(err);
|
|
77
|
+
});
|
|
78
|
+
archive.on('error', reject);
|
|
79
|
+
|
|
80
|
+
archive.pipe(output);
|
|
81
|
+
// 关键:zip 内顶层目录名强制为 buildDirName
|
|
82
|
+
archive.directory(srcDir, topFolderName);
|
|
83
|
+
archive.finalize();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hashLettersFromSha256(buf, lettersLen = 12) {
|
|
88
|
+
const alphabet = 'abcdefghijklmnop'; // 16 letters for 0..15
|
|
89
|
+
let out = '';
|
|
90
|
+
for (let i = 0; i < buf.length && out.length < lettersLen; i++) {
|
|
91
|
+
const b = buf[i];
|
|
92
|
+
out += alphabet[(b >> 4) & 0xf];
|
|
93
|
+
if (out.length >= lettersLen) break;
|
|
94
|
+
out += alphabet[b & 0xf];
|
|
95
|
+
}
|
|
96
|
+
return out.slice(0, lettersLen);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function computeDirHashLetters(localDir) {
|
|
100
|
+
const crypto = require('crypto');
|
|
101
|
+
const h = crypto.createHash('sha256');
|
|
102
|
+
const files = listLocalFilesWithSize(localDir);
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
h.update(f.relPosix);
|
|
105
|
+
h.update('\n');
|
|
106
|
+
h.update(String(f.size || 0));
|
|
107
|
+
h.update('\n');
|
|
108
|
+
h.update(String(Math.floor(f.mtimeMs || 0)));
|
|
109
|
+
h.update('\n');
|
|
110
|
+
}
|
|
111
|
+
const digest = h.digest();
|
|
112
|
+
return hashLettersFromSha256(digest, 12);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function chooseDeployEnv(env) {
|
|
116
|
+
if (env === 'test' || env === 'prod') return env;
|
|
117
|
+
const ans = await inquirer.prompt([
|
|
118
|
+
{
|
|
119
|
+
type: 'list',
|
|
120
|
+
name: 'env',
|
|
121
|
+
message: '请选择部署环境',
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: '部署测试服(上传到服务器)', value: 'test' },
|
|
124
|
+
{ name: '部署正式服(不上传,仅压缩)', value: 'prod' },
|
|
125
|
+
],
|
|
126
|
+
default: 'test',
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
return ans.env;
|
|
130
|
+
}
|
|
131
|
+
|
|
56
132
|
async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
|
|
57
133
|
const files = listLocalFilesWithSize(localDir);
|
|
58
134
|
const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
@@ -124,6 +200,8 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
124
200
|
const deploy = (projectConfig && projectConfig.deploy) || {};
|
|
125
201
|
|
|
126
202
|
const cfg = {
|
|
203
|
+
env: overrides.env ?? deploy.env,
|
|
204
|
+
buildDir: overrides.buildDir ?? deploy.buildDir ?? projectConfig.buildDir,
|
|
127
205
|
host: overrides.host ?? deploy.host,
|
|
128
206
|
port: String(overrides.port ?? deploy.port ?? '22'),
|
|
129
207
|
username: overrides.username ?? deploy.username,
|
|
@@ -139,21 +217,51 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
139
217
|
|
|
140
218
|
/**
|
|
141
219
|
* 执行部署(递归上传目录到远端)
|
|
142
|
-
*
|
|
220
|
+
* 测试服:上传到服务器(可选部署后压缩)
|
|
221
|
+
* 正式服:不上传,仅压缩
|
|
222
|
+
* @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
|
|
143
223
|
*/
|
|
144
224
|
async function deploy(overrides = {}) {
|
|
145
225
|
const cfg = resolveDeployConfig(overrides);
|
|
146
226
|
const localFolderPath = cfg.localDir;
|
|
147
227
|
const remoteFolderPath = cfg.remoteDir;
|
|
148
228
|
|
|
229
|
+
const env = await chooseDeployEnv(cfg.env);
|
|
230
|
+
|
|
149
231
|
if (!fs.existsSync(localFolderPath)) {
|
|
150
232
|
throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
|
|
151
233
|
}
|
|
152
234
|
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
235
|
+
// 正式服:仅压缩
|
|
236
|
+
if (env === 'prod') {
|
|
237
|
+
const buildDirName = sanitizeBuildDirName(
|
|
238
|
+
cfg.buildDir != null && cfg.buildDir !== ''
|
|
239
|
+
? String(cfg.buildDir).replace(/\//g, '').replace(/\\/g, '')
|
|
240
|
+
: path.basename(path.resolve(localFolderPath)).replace(/[\\/]/g, '')
|
|
241
|
+
);
|
|
242
|
+
const ts = formatTimestampCompact(new Date());
|
|
243
|
+
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
244
|
+
const zipBaseName = `${buildDirName}${ts}${hashLetters}`;
|
|
245
|
+
fs.ensureDirSync(cfg.zipOutDir);
|
|
246
|
+
const zipName = `${zipBaseName}.zip`;
|
|
247
|
+
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
248
|
+
|
|
249
|
+
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
250
|
+
try {
|
|
251
|
+
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
252
|
+
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
zipSpinner.fail('压缩失败');
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 测试服:上传到服务器
|
|
261
|
+
if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
|
|
262
|
+
if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
|
|
263
|
+
if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
|
|
264
|
+
if (!remoteFolderPath) throw new Error('缺少 deploy.remoteDir(测试服上传需要)');
|
|
157
265
|
|
|
158
266
|
let Client;
|
|
159
267
|
try {
|
|
@@ -180,7 +288,7 @@ async function deploy(overrides = {}) {
|
|
|
180
288
|
try {
|
|
181
289
|
await sftp.rmdir(remoteFolderPath, true);
|
|
182
290
|
} catch (e) {
|
|
183
|
-
//
|
|
291
|
+
// ignore
|
|
184
292
|
}
|
|
185
293
|
await sftp.mkdir(remoteFolderPath, true);
|
|
186
294
|
|
|
@@ -207,25 +315,23 @@ async function deploy(overrides = {}) {
|
|
|
207
315
|
if (spinner) spinner.info('部署结束');
|
|
208
316
|
}
|
|
209
317
|
|
|
210
|
-
//
|
|
318
|
+
// 测试服可选:部署后压缩(延续之前的可选开关)
|
|
211
319
|
if (cfg.zipAfter) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const buildDirName = path.basename(path.resolve(localFolderPath));
|
|
320
|
+
const buildDirName = sanitizeBuildDirName(
|
|
321
|
+
cfg.buildDir != null && cfg.buildDir !== ''
|
|
322
|
+
? String(cfg.buildDir).replace(/\//g, '').replace(/\\/g, '')
|
|
323
|
+
: path.basename(path.resolve(localFolderPath)).replace(/[\\/]/g, '')
|
|
324
|
+
);
|
|
325
|
+
const ts = formatTimestampCompact(new Date());
|
|
326
|
+
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
327
|
+
const zipBaseName = `${buildDirName}${ts}${hashLetters}`;
|
|
222
328
|
fs.ensureDirSync(cfg.zipOutDir);
|
|
223
|
-
const zipName = `${
|
|
329
|
+
const zipName = `${zipBaseName}.zip`;
|
|
224
330
|
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
225
331
|
|
|
226
332
|
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
227
333
|
try {
|
|
228
|
-
await
|
|
334
|
+
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
229
335
|
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
230
336
|
} catch (e) {
|
|
231
337
|
zipSpinner.fail('压缩失败');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylyx-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "公司内部代码生成模板脚手架工具,支持快速生成项目初始结构和代码模板",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"node": ">=12.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"archiver": "^5.3.2",
|
|
46
47
|
"commander": "^11.1.0",
|
|
47
|
-
"compressing": "^1.10.1",
|
|
48
48
|
"inquirer": "^8.2.6",
|
|
49
49
|
"chalk": "^4.1.2",
|
|
50
50
|
"fs-extra": "^11.2.0",
|