@struggler/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/command/upload.js +60 -9
- package/index.js +1 -1
- package/lib/cache.js +37 -0
- package/lib/config.js +13 -0
- package/lib/deploy.js +56 -3
- package/lib/i18n.js +2 -0
- package/lib/progress.js +82 -0
- package/package.json +1 -1
package/command/upload.js
CHANGED
|
@@ -2,7 +2,7 @@ var qiniu = require("qiniu");
|
|
|
2
2
|
|
|
3
3
|
var qiniuPrefix = require("../lib/prefix")
|
|
4
4
|
|
|
5
|
-
let { getQiniuConfig, getDir } = require('../lib/config')
|
|
5
|
+
let { getQiniuConfig, getDir, getCachePath } = require('../lib/config')
|
|
6
6
|
let { getJsonData } = require('../lib/files')
|
|
7
7
|
const { createIgnoreMatcher } = require('../lib/ignore');
|
|
8
8
|
const {
|
|
@@ -16,6 +16,8 @@ const {
|
|
|
16
16
|
toRemoteKey,
|
|
17
17
|
} = require('../lib/deploy');
|
|
18
18
|
const { printMessage } = require('../lib/output');
|
|
19
|
+
const { computeFileMd5, readCache, writeCache, isCacheHit, updateCacheEntry } = require('../lib/cache');
|
|
20
|
+
const { createProgressBar } = require('../lib/progress');
|
|
19
21
|
|
|
20
22
|
async function main(options, runtime = {}) {
|
|
21
23
|
const qiniuConfig = getJsonData(getQiniuConfig(options))
|
|
@@ -25,20 +27,39 @@ async function main(options, runtime = {}) {
|
|
|
25
27
|
const ignoreMatcher = createIgnoreMatcher(dir, options);
|
|
26
28
|
const excludedPatterns = runtime.excludePatterns || ignoreMatcher.patterns;
|
|
27
29
|
const files = runtime.files || await collectDeployFiles(dir, options);
|
|
28
|
-
|
|
30
|
+
|
|
31
|
+
const useCache = !options.noCache;
|
|
32
|
+
const cachePath = getCachePath(options);
|
|
33
|
+
const cache = useCache ? readCache(cachePath) : {};
|
|
34
|
+
|
|
35
|
+
const plans = [];
|
|
36
|
+
const skippedPlans = [];
|
|
37
|
+
|
|
38
|
+
for (const localFile of files) {
|
|
29
39
|
const key = toRemoteKey(prefix, dir, localFile);
|
|
30
|
-
|
|
40
|
+
const plan = {
|
|
31
41
|
localFile,
|
|
32
42
|
key,
|
|
33
43
|
target: `${qiniuConfig.domain || ''}${key}`,
|
|
34
44
|
};
|
|
35
|
-
|
|
45
|
+
|
|
46
|
+
if (useCache && !options.dryRun) {
|
|
47
|
+
const md5 = computeFileMd5(localFile);
|
|
48
|
+
plan.localMd5 = md5;
|
|
49
|
+
if (isCacheHit(cache, key, md5)) {
|
|
50
|
+
skippedPlans.push(plan);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
plans.push(plan);
|
|
56
|
+
}
|
|
36
57
|
|
|
37
58
|
if (options.dryRun) {
|
|
38
59
|
if (!runtime.suppressOutput) {
|
|
39
|
-
logPlan('upload', plans, options);
|
|
60
|
+
logPlan('upload', [...plans, ...skippedPlans], options);
|
|
40
61
|
}
|
|
41
|
-
const summary = createSummary('upload', true, plans.map((plan) => ({
|
|
62
|
+
const summary = createSummary('upload', true, [...plans, ...skippedPlans].map((plan) => ({
|
|
42
63
|
ok: true,
|
|
43
64
|
localFile: plan.localFile,
|
|
44
65
|
key: plan.key,
|
|
@@ -47,6 +68,7 @@ async function main(options, runtime = {}) {
|
|
|
47
68
|
prefix,
|
|
48
69
|
concurrency: normalizeConcurrency(options.concurrency),
|
|
49
70
|
excludedPatterns,
|
|
71
|
+
skippedCount: 0,
|
|
50
72
|
});
|
|
51
73
|
return runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
|
|
52
74
|
}
|
|
@@ -102,11 +124,22 @@ async function main(options, runtime = {}) {
|
|
|
102
124
|
});
|
|
103
125
|
}
|
|
104
126
|
|
|
127
|
+
const totalFiles = plans.length + skippedPlans.length;
|
|
128
|
+
const bar = createProgressBar(totalFiles, {
|
|
129
|
+
json: options.json,
|
|
130
|
+
suppressOutput: runtime.suppressOutput,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
for (const plan of skippedPlans) {
|
|
134
|
+
bar.tick({ filename: plan.localFile, skipped: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
105
137
|
const results = await runWithConcurrency(plans, normalizeConcurrency(options.concurrency), async (plan) => {
|
|
106
138
|
try {
|
|
107
139
|
const response = await upload(plan);
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
bar.tick({ filename: plan.localFile });
|
|
141
|
+
if (useCache && plan.localMd5) {
|
|
142
|
+
updateCacheEntry(cache, plan.key, plan.localMd5, response.hash);
|
|
110
143
|
}
|
|
111
144
|
return {
|
|
112
145
|
ok: true,
|
|
@@ -116,6 +149,7 @@ async function main(options, runtime = {}) {
|
|
|
116
149
|
hash: response.hash,
|
|
117
150
|
};
|
|
118
151
|
} catch (error) {
|
|
152
|
+
bar.tick({ filename: plan.localFile, failed: true });
|
|
119
153
|
return {
|
|
120
154
|
ok: false,
|
|
121
155
|
localFile: plan.localFile,
|
|
@@ -126,10 +160,27 @@ async function main(options, runtime = {}) {
|
|
|
126
160
|
}
|
|
127
161
|
});
|
|
128
162
|
|
|
129
|
-
|
|
163
|
+
bar.finish();
|
|
164
|
+
|
|
165
|
+
if (useCache) {
|
|
166
|
+
writeCache(cachePath, cache);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const skippedResults = skippedPlans.map((plan) => ({
|
|
170
|
+
ok: true,
|
|
171
|
+
skipped: true,
|
|
172
|
+
localFile: plan.localFile,
|
|
173
|
+
key: plan.key,
|
|
174
|
+
target: plan.target,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const allResults = [...results, ...skippedResults];
|
|
178
|
+
|
|
179
|
+
const summary = createSummary('upload', false, allResults, startedAt, {
|
|
130
180
|
prefix,
|
|
131
181
|
concurrency: normalizeConcurrency(options.concurrency),
|
|
132
182
|
excludedPatterns,
|
|
183
|
+
skippedCount: skippedPlans.length,
|
|
133
184
|
});
|
|
134
185
|
const finalSummary = runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
|
|
135
186
|
if (finalSummary.failedCount > 0) {
|
package/index.js
CHANGED
|
@@ -60,7 +60,7 @@ program.configureHelp({
|
|
|
60
60
|
},
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
program.name("struggler-cli").description(locale.appDescription).version(packageJson.version, "-v, --version", locale.options.version).helpOption("-h, --help", locale.options.help).option("-c, --config <path>", locale.options.config, "./command/qiniu.json").option("-d, --dir <path>", locale.options.dir, "./dist").option("--dry-run", locale.options.dryRun).option("--concurrency <number>", locale.options.concurrency, "5").option("--exclude <pattern>", locale.options.exclude).option("--ignore-file <path>", locale.options.ignoreFile, ".strugglerignore").option("--manifest <path>", locale.options.manifest).option("--json", locale.options.json).option("--skip-init", locale.options.skipInit).option("--skip-refresh", locale.options.skipRefresh).option("--lang <lang>", locale.options.lang, lang)
|
|
63
|
+
program.name("struggler-cli").description(locale.appDescription).version(packageJson.version, "-v, --version", locale.options.version).helpOption("-h, --help", locale.options.help).option("-c, --config <path>", locale.options.config, "./command/qiniu.json").option("-d, --dir <path>", locale.options.dir, "./dist").option("--dry-run", locale.options.dryRun).option("--concurrency <number>", locale.options.concurrency, "5").option("--exclude <pattern>", locale.options.exclude).option("--ignore-file <path>", locale.options.ignoreFile, ".strugglerignore").option("--manifest <path>", locale.options.manifest).option("--json", locale.options.json).option("--skip-init", locale.options.skipInit).option("--skip-refresh", locale.options.skipRefresh).option("--no-cache", locale.options.noCache).option("--lang <lang>", locale.options.lang, lang)
|
|
64
64
|
|
|
65
65
|
program
|
|
66
66
|
.command("init")
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const { getJsonData, setSyncJsonData } = require('./files');
|
|
4
|
+
|
|
5
|
+
function computeFileMd5(filePath) {
|
|
6
|
+
const content = fs.readFileSync(filePath);
|
|
7
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readCache(cachePath) {
|
|
11
|
+
return getJsonData(cachePath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeCache(cachePath, cache) {
|
|
15
|
+
setSyncJsonData(cachePath, cache);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isCacheHit(cache, relativeKey, localMd5) {
|
|
19
|
+
const entry = cache[relativeKey];
|
|
20
|
+
return entry && entry.localMd5 === localMd5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function updateCacheEntry(cache, relativeKey, localMd5, qiniuHash) {
|
|
24
|
+
cache[relativeKey] = {
|
|
25
|
+
localMd5,
|
|
26
|
+
qiniuHash,
|
|
27
|
+
uploadedAt: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
computeFileMd5,
|
|
33
|
+
readCache,
|
|
34
|
+
writeCache,
|
|
35
|
+
isCacheHit,
|
|
36
|
+
updateCacheEntry,
|
|
37
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -2,6 +2,7 @@ let path = require('path')
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_QINIU_CONFIG = './command/qiniu.json';
|
|
4
4
|
const DEFAULT_CONFIG = './command/config.json';
|
|
5
|
+
const DEFAULT_CACHE = './command/upload-cache.json';
|
|
5
6
|
|
|
6
7
|
function resolveFromCwd(targetPath) {
|
|
7
8
|
return path.resolve(process.cwd(), targetPath);
|
|
@@ -20,6 +21,15 @@ function getConfigPath(options = {}) {
|
|
|
20
21
|
return path.join(path.dirname(qiniuConfigPath), 'config.json');
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function getCachePath(options = {}) {
|
|
25
|
+
if (!options.config) {
|
|
26
|
+
return resolveFromCwd(DEFAULT_CACHE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const qiniuConfigPath = getQiniuConfigPath(options);
|
|
30
|
+
return path.join(path.dirname(qiniuConfigPath), 'upload-cache.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
module.exports = {
|
|
24
34
|
getConfig: (options) => {
|
|
25
35
|
return getConfigPath(options)
|
|
@@ -27,6 +37,9 @@ module.exports = {
|
|
|
27
37
|
getQiniuConfig: (options) => {
|
|
28
38
|
return getQiniuConfigPath(options)
|
|
29
39
|
},
|
|
40
|
+
getCachePath: (options) => {
|
|
41
|
+
return getCachePath(options)
|
|
42
|
+
},
|
|
30
43
|
getDir: (options) => {
|
|
31
44
|
return resolveFromCwd(options.dir || './dist')
|
|
32
45
|
},
|
package/lib/deploy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const chalk = require('chalk');
|
|
2
3
|
const { listFiles } = require('./files');
|
|
3
4
|
const { createIgnoreMatcher } = require('./ignore');
|
|
4
5
|
const { printMessage, writeManifest, shouldUseJson, printJson } = require('./output');
|
|
@@ -70,13 +71,65 @@ function logPlan(action, items, options) {
|
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function formatDuration(ms) {
|
|
75
|
+
if (ms < 1000) return `${ms}ms`;
|
|
76
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
function logSummary(summary, options) {
|
|
74
|
-
const
|
|
75
|
-
|
|
80
|
+
const allOk = summary.failedCount === 0;
|
|
81
|
+
const isDryRun = summary.dryRun;
|
|
82
|
+
const skipped = summary.skippedCount || 0;
|
|
83
|
+
const uploaded = summary.succeededCount - skipped;
|
|
84
|
+
|
|
85
|
+
const statusIcon = allOk ? chalk.green('✓') : chalk.red('✗');
|
|
86
|
+
const actionLabel = summary.action === 'upload' ? '上传' : summary.action === 'refresh' ? '刷新' : summary.action;
|
|
87
|
+
const modeTag = isDryRun ? chalk.yellow(' [dry-run]') : '';
|
|
88
|
+
|
|
89
|
+
printMessage(options, '');
|
|
90
|
+
printMessage(options, ` ${statusIcon} ${chalk.bold(actionLabel + '完成')}${modeTag} ${chalk.dim(formatDuration(summary.durationMs))}`);
|
|
91
|
+
printMessage(options, '');
|
|
92
|
+
|
|
93
|
+
const rows = [];
|
|
94
|
+
if (summary.action === 'upload') {
|
|
95
|
+
rows.push([chalk.dim('上传'), chalk.green(`${uploaded} 个文件`)]);
|
|
96
|
+
if (skipped > 0) {
|
|
97
|
+
rows.push([chalk.dim('跳过'), chalk.cyan(`${skipped} 个文件`) + chalk.dim(' (缓存命中,无变更)')]);
|
|
98
|
+
}
|
|
99
|
+
if (summary.failedCount > 0) {
|
|
100
|
+
rows.push([chalk.dim('失败'), chalk.red(`${summary.failedCount} 个文件`)]);
|
|
101
|
+
}
|
|
102
|
+
rows.push([chalk.dim('合计'), chalk.white(`${summary.total} 个文件`)]);
|
|
103
|
+
} else {
|
|
104
|
+
rows.push([chalk.dim('成功'), chalk.green(`${summary.succeededCount} 个`)]);
|
|
105
|
+
if (summary.failedCount > 0) {
|
|
106
|
+
rows.push([chalk.dim('失败'), chalk.red(`${summary.failedCount} 个`)]);
|
|
107
|
+
}
|
|
108
|
+
rows.push([chalk.dim('合计'), chalk.white(`${summary.total} 个`)]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const labelWidth = 4;
|
|
112
|
+
rows.forEach(([label, value]) => {
|
|
113
|
+
printMessage(options, ` ${label.padEnd ? label : label} ${value}`);
|
|
114
|
+
});
|
|
115
|
+
printMessage(options, '');
|
|
116
|
+
|
|
76
117
|
if (summary.failedCount > 0) {
|
|
77
118
|
summary.failed.forEach((item) => {
|
|
78
|
-
printMessage(options, `
|
|
119
|
+
printMessage(options, ` ${chalk.red('✗')} ${chalk.dim(item.localFile)}`);
|
|
120
|
+
printMessage(options, ` ${chalk.red(item.error)}`);
|
|
121
|
+
});
|
|
122
|
+
printMessage(options, '');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const uploadedItems = (summary.succeeded || []).filter((item) => !item.skipped && item.target);
|
|
126
|
+
if (uploadedItems.length > 0) {
|
|
127
|
+
printMessage(options, ` ${chalk.dim('─── 已上传文件链接 ───')}`);
|
|
128
|
+
printMessage(options, '');
|
|
129
|
+
uploadedItems.forEach((item) => {
|
|
130
|
+
printMessage(options, ` ${chalk.cyan(item.target)}`);
|
|
79
131
|
});
|
|
132
|
+
printMessage(options, '');
|
|
80
133
|
}
|
|
81
134
|
}
|
|
82
135
|
|
package/lib/i18n.js
CHANGED
|
@@ -13,6 +13,7 @@ const LANGUAGES = {
|
|
|
13
13
|
json: '输出机器可读的 JSON 结果。',
|
|
14
14
|
skipInit: '在 deploy 时跳过 init 步骤。',
|
|
15
15
|
skipRefresh: '在 deploy 时跳过 refresh 步骤。',
|
|
16
|
+
noCache: '禁用上传缓存,强制重新上传所有文件。',
|
|
16
17
|
lang: '切换菜单语言,可选 zh / en。',
|
|
17
18
|
help: '显示帮助信息。',
|
|
18
19
|
},
|
|
@@ -51,6 +52,7 @@ const LANGUAGES = {
|
|
|
51
52
|
json: 'Print machine-readable JSON output.',
|
|
52
53
|
skipInit: 'Skip init during deploy.',
|
|
53
54
|
skipRefresh: 'Skip refresh during deploy.',
|
|
55
|
+
noCache: 'Disable upload cache and force re-upload all files.',
|
|
54
56
|
lang: 'Switch menu language, supported values: zh / en.',
|
|
55
57
|
help: 'Display help information.',
|
|
56
58
|
},
|
package/lib/progress.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
const BAR_WIDTH = 30;
|
|
4
|
+
|
|
5
|
+
function buildBar(completed, total) {
|
|
6
|
+
const ratio = total === 0 ? 1 : completed / total;
|
|
7
|
+
const filled = Math.round(BAR_WIDTH * ratio);
|
|
8
|
+
const empty = BAR_WIDTH - filled;
|
|
9
|
+
return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function truncateFilename(filename, maxLen) {
|
|
13
|
+
if (!filename || filename.length <= maxLen) {
|
|
14
|
+
return (filename || '').padEnd(maxLen);
|
|
15
|
+
}
|
|
16
|
+
return '...' + filename.slice(-(maxLen - 3));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createProgressBar(total, options = {}) {
|
|
20
|
+
if (options.json || options.suppressOutput || total === 0) {
|
|
21
|
+
return { tick: () => {}, finish: () => {} };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let completed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
let lastLine = '';
|
|
28
|
+
|
|
29
|
+
const isTTY = process.stdout.isTTY;
|
|
30
|
+
|
|
31
|
+
function render(currentFile, status) {
|
|
32
|
+
const bar = buildBar(completed, total);
|
|
33
|
+
const pct = total === 0 ? 100 : Math.round((completed / total) * 100);
|
|
34
|
+
const cols = (process.stdout.columns || 80) - BAR_WIDTH - 30;
|
|
35
|
+
const nameWidth = Math.max(10, Math.min(40, cols));
|
|
36
|
+
const name = truncateFilename(currentFile, nameWidth);
|
|
37
|
+
|
|
38
|
+
let statusIcon;
|
|
39
|
+
if (status === 'skipped') {
|
|
40
|
+
statusIcon = chalk.cyan('↩');
|
|
41
|
+
} else if (status === 'failed') {
|
|
42
|
+
statusIcon = chalk.red('✗');
|
|
43
|
+
} else {
|
|
44
|
+
statusIcon = chalk.green('✓');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const countStr = chalk.white(`${completed}/${total}`);
|
|
48
|
+
const pctStr = chalk.yellow(`${pct}%`);
|
|
49
|
+
const line = ` ${bar} ${countStr} ${pctStr} ${statusIcon} ${chalk.dim(name)}`;
|
|
50
|
+
|
|
51
|
+
if (isTTY) {
|
|
52
|
+
process.stdout.write('\r' + line.padEnd(lastLine.length));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(line);
|
|
55
|
+
}
|
|
56
|
+
lastLine = line;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tick({ filename = '', skipped: isSkipped = false, failed: isFailed = false } = {}) {
|
|
60
|
+
completed += 1;
|
|
61
|
+
if (isSkipped) skipped += 1;
|
|
62
|
+
if (isFailed) failed += 1;
|
|
63
|
+
render(filename, isSkipped ? 'skipped' : isFailed ? 'failed' : 'uploaded');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function finish() {
|
|
67
|
+
if (!isTTY) return;
|
|
68
|
+
const bar = buildBar(total, total);
|
|
69
|
+
const parts = [
|
|
70
|
+
` ${bar}`,
|
|
71
|
+
chalk.white(`${total}/${total}`),
|
|
72
|
+
chalk.green('100%'),
|
|
73
|
+
];
|
|
74
|
+
if (skipped > 0) parts.push(chalk.cyan(`${skipped} skipped`));
|
|
75
|
+
if (failed > 0) parts.push(chalk.red(`${failed} failed`));
|
|
76
|
+
process.stdout.write('\r' + parts.join(' ').padEnd(lastLine.length) + '\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { tick, finish };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { createProgressBar };
|