@struggler/cli 1.0.2 → 1.0.5
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/.strugglerignore +6 -0
- package/Makefile +81 -0
- package/README.md +124 -0
- package/command/addVersion.js +11 -5
- package/command/deploy.js +75 -0
- package/command/index.js +4 -2
- package/command/init.js +20 -20
- package/command/refresh.js +111 -0
- package/command/upload.js +108 -169
- package/index.js +84 -23
- package/lib/config.js +22 -4
- package/lib/date.js +14 -0
- package/lib/deploy.js +132 -0
- package/lib/files.js +30 -4
- package/lib/i18n.js +189 -0
- package/lib/ignore.js +69 -0
- package/lib/output.js +37 -0
- package/lib/prefix.js +2 -16
- package/package.json +28 -27
- package/test/config.test.js +41 -0
- package/test/init.test.js +52 -0
- package/test/test.sh +2 -2
- package/test/upload.test.js +113 -0
package/command/upload.js
CHANGED
|
@@ -1,203 +1,142 @@
|
|
|
1
|
-
var fs = require('fs');
|
|
2
|
-
|
|
3
|
-
var path = require('path');
|
|
4
|
-
|
|
5
1
|
var qiniu = require("qiniu");
|
|
6
2
|
|
|
7
3
|
var qiniuPrefix = require("../lib/prefix")
|
|
8
4
|
|
|
9
5
|
let { getQiniuConfig, getDir } = require('../lib/config')
|
|
10
|
-
let { getJsonData
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
let { getJsonData } = require('../lib/files')
|
|
7
|
+
const { createIgnoreMatcher } = require('../lib/ignore');
|
|
8
|
+
const {
|
|
9
|
+
collectDeployFiles,
|
|
10
|
+
createSummary,
|
|
11
|
+
ensureRequiredConfig,
|
|
12
|
+
finalizeOutput,
|
|
13
|
+
logPlan,
|
|
14
|
+
normalizeConcurrency,
|
|
15
|
+
runWithConcurrency,
|
|
16
|
+
toRemoteKey,
|
|
17
|
+
} = require('../lib/deploy');
|
|
18
|
+
const { printMessage } = require('../lib/output');
|
|
19
|
+
|
|
20
|
+
async function main(options, runtime = {}) {
|
|
14
21
|
const qiniuConfig = getJsonData(getQiniuConfig(options))
|
|
22
|
+
const prefix = runtime.prefix || qiniuPrefix.prefix(options);
|
|
23
|
+
let dir = getDir(options)
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
const ignoreMatcher = createIgnoreMatcher(dir, options);
|
|
26
|
+
const excludedPatterns = runtime.excludePatterns || ignoreMatcher.patterns;
|
|
27
|
+
const files = runtime.files || await collectDeployFiles(dir, options);
|
|
28
|
+
const plans = files.map((localFile) => {
|
|
29
|
+
const key = toRemoteKey(prefix, dir, localFile);
|
|
30
|
+
return {
|
|
31
|
+
localFile,
|
|
32
|
+
key,
|
|
33
|
+
target: `${qiniuConfig.domain || ''}${key}`,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (options.dryRun) {
|
|
38
|
+
if (!runtime.suppressOutput) {
|
|
39
|
+
logPlan('upload', plans, options);
|
|
40
|
+
}
|
|
41
|
+
const summary = createSummary('upload', true, plans.map((plan) => ({
|
|
42
|
+
ok: true,
|
|
43
|
+
localFile: plan.localFile,
|
|
44
|
+
key: plan.key,
|
|
45
|
+
target: plan.target,
|
|
46
|
+
})), startedAt, {
|
|
47
|
+
prefix,
|
|
48
|
+
concurrency: normalizeConcurrency(options.concurrency),
|
|
49
|
+
excludedPatterns,
|
|
50
|
+
});
|
|
51
|
+
return runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
|
|
52
|
+
}
|
|
15
53
|
|
|
16
|
-
|
|
54
|
+
ensureRequiredConfig(
|
|
55
|
+
{ ...qiniuConfig, 'publicPath(config.json)': prefix },
|
|
56
|
+
['accessKey', 'secretKey', 'Bucket', 'zone', 'domain', 'publicPath(config.json)']
|
|
57
|
+
);
|
|
17
58
|
|
|
18
59
|
var accessKey = qiniuConfig.accessKey
|
|
19
|
-
|
|
20
60
|
var secretKey = qiniuConfig.secretKey;
|
|
21
|
-
|
|
22
61
|
var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
|
23
|
-
|
|
24
62
|
var config = new qiniu.conf.Config();
|
|
25
|
-
|
|
26
|
-
// 空间对应的机房,zone_z1代表华北,其他配置参见七牛云文档
|
|
27
|
-
|
|
28
63
|
config.zone = qiniu.zone[qiniuConfig.zone];
|
|
29
|
-
|
|
30
|
-
// 是否使用https域名
|
|
31
|
-
|
|
32
64
|
config.useHttpsDomain = true;
|
|
33
|
-
|
|
34
|
-
// 上传是否使用cdn加速
|
|
35
|
-
|
|
36
65
|
config.useCdnDomain = true;
|
|
37
66
|
|
|
38
67
|
var formUploader = new qiniu.form_up.FormUploader(config);
|
|
39
|
-
|
|
40
68
|
var putExtra = new qiniu.form_up.PutExtra();
|
|
41
69
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let dir = getDir(options)
|
|
47
|
-
|
|
48
|
-
function upload(key, localFile) {
|
|
49
|
-
//windows
|
|
50
|
-
|
|
51
|
-
/* let str = null;
|
|
52
|
-
|
|
53
|
-
if (localFile.indexOf("./dist\\") >= 0) {
|
|
54
|
-
|
|
55
|
-
str = localFile.replace("./dist\\", "");
|
|
56
|
-
|
|
57
|
-
} else if (localFile.indexOf("./dist/") >= 0) {
|
|
58
|
-
|
|
59
|
-
//苹果
|
|
60
|
-
|
|
61
|
-
str = localFile.replace("./dist/", "");
|
|
62
|
-
|
|
63
|
-
} else {
|
|
64
|
-
|
|
65
|
-
str = localFile;
|
|
66
|
-
|
|
67
|
-
} */
|
|
68
|
-
|
|
69
|
-
const str = path.relative(dir, localFile)
|
|
70
|
-
|
|
71
|
-
key = prefix + str
|
|
72
|
-
|
|
73
|
-
//上传之后的文件名
|
|
74
|
-
key = key.replace(/\\/g, "/")
|
|
75
|
-
|
|
76
|
-
//这里base-html是存储空间名
|
|
77
|
-
|
|
78
|
-
// var Bucket = `cfun:${key}`;
|
|
79
|
-
|
|
80
|
-
var Bucket = qiniuConfig.Bucket;
|
|
81
|
-
|
|
82
|
-
var options = {
|
|
83
|
-
|
|
84
|
-
// scope: Bucket,
|
|
85
|
-
|
|
86
|
-
// https://developer.qiniu.com/kodo/1289/nodejs#overwrite-uptoken
|
|
87
|
-
scope: Bucket + ":" + key
|
|
88
|
-
|
|
89
|
-
// detectMime:0
|
|
90
|
-
|
|
91
|
-
// MimeType: 'text/html;text/css;text/javascript;application/x-gzip',
|
|
92
|
-
|
|
70
|
+
function upload(plan, attempt = 1) {
|
|
71
|
+
var uploadOptions = {
|
|
72
|
+
scope: `${qiniuConfig.Bucket}:${plan.key}`
|
|
93
73
|
};
|
|
94
|
-
|
|
95
|
-
var putPolicy = new qiniu.rs.PutPolicy(options);
|
|
96
|
-
|
|
74
|
+
var putPolicy = new qiniu.rs.PutPolicy(uploadOptions);
|
|
97
75
|
var uploadToken = putPolicy.uploadToken(mac);
|
|
98
76
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// console.log(respBody);
|
|
112
|
-
|
|
113
|
-
upload('file2', localFile);
|
|
114
|
-
|
|
115
|
-
throw respErr;
|
|
116
|
-
|
|
117
|
-
} else {
|
|
118
|
-
|
|
119
|
-
if (respInfo.statusCode == 200) {
|
|
120
|
-
respBody.key = qiniuConfig.domain + respBody.key
|
|
121
|
-
|
|
122
|
-
console.log(respBody);
|
|
123
|
-
|
|
124
|
-
} else {
|
|
125
|
-
|
|
126
|
-
console.log(respInfo.statusCode);
|
|
127
|
-
|
|
128
|
-
console.log(respBody);
|
|
129
|
-
|
|
130
|
-
if (respBody.error) {
|
|
131
|
-
|
|
132
|
-
console.log(respBody.error)
|
|
133
|
-
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
formUploader.putFile(uploadToken, plan.key, plan.localFile, putExtra, async function (respErr, respBody, respInfo) {
|
|
79
|
+
if (respErr || respInfo.statusCode !== 200) {
|
|
80
|
+
if (attempt < 3) {
|
|
81
|
+
console.log(`${plan.localFile} 上传失败,正在进行第 ${attempt + 1} 次重试`);
|
|
82
|
+
try {
|
|
83
|
+
const retryResult = await upload(plan, attempt + 1);
|
|
84
|
+
resolve(retryResult);
|
|
85
|
+
} catch (retryError) {
|
|
86
|
+
reject(retryError);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
134
89
|
}
|
|
135
90
|
|
|
91
|
+
const errorMessage = respErr || (respBody && respBody.error) || `statusCode=${respInfo && respInfo.statusCode}`;
|
|
92
|
+
reject(new Error(errorMessage));
|
|
93
|
+
return;
|
|
136
94
|
}
|
|
137
95
|
|
|
138
|
-
|
|
139
|
-
|
|
96
|
+
resolve({
|
|
97
|
+
hash: respBody.hash,
|
|
98
|
+
key: plan.key,
|
|
99
|
+
target: plan.target,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
140
102
|
});
|
|
141
|
-
|
|
142
103
|
}
|
|
143
104
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//var param = path.resolve(param);
|
|
151
|
-
|
|
152
|
-
fs.stat(param, function (err, stats) {
|
|
153
|
-
|
|
154
|
-
//如果是目录的话,遍历目录下的文件信息
|
|
155
|
-
|
|
156
|
-
if (stats.isDirectory()) {
|
|
157
|
-
|
|
158
|
-
fs.readdir(param, function (err, file) {
|
|
159
|
-
|
|
160
|
-
file.forEach((e) => {
|
|
161
|
-
|
|
162
|
-
//遍历之后递归调用查看文件函数
|
|
163
|
-
|
|
164
|
-
//遍历目录得到的文件名称是不含路径的,需要将前面的绝对路径拼接
|
|
165
|
-
|
|
166
|
-
var absolutePath = path.join(param, e);
|
|
167
|
-
|
|
168
|
-
//var absolutePath = path.resolve(path.join(param, e));
|
|
169
|
-
|
|
170
|
-
displayFile(absolutePath)
|
|
171
|
-
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
} else {
|
|
177
|
-
|
|
178
|
-
//file2/这里是空间里的文件前缀
|
|
179
|
-
|
|
180
|
-
var key = 'file2';
|
|
181
|
-
|
|
182
|
-
var localFile = param;
|
|
183
|
-
|
|
184
|
-
if (!localFile.endsWith(".gz")) {
|
|
185
|
-
|
|
186
|
-
upload(key, localFile);
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
105
|
+
const results = await runWithConcurrency(plans, normalizeConcurrency(options.concurrency), async (plan) => {
|
|
106
|
+
try {
|
|
107
|
+
const response = await upload(plan);
|
|
108
|
+
if (!runtime.suppressOutput) {
|
|
109
|
+
printMessage(options, `[uploaded] ${plan.localFile} -> ${plan.target}`);
|
|
190
110
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
localFile: plan.localFile,
|
|
114
|
+
key: response.key,
|
|
115
|
+
target: response.target,
|
|
116
|
+
hash: response.hash,
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
localFile: plan.localFile,
|
|
122
|
+
key: plan.key,
|
|
123
|
+
target: plan.target,
|
|
124
|
+
error: error.message,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const summary = createSummary('upload', false, results, startedAt, {
|
|
130
|
+
prefix,
|
|
131
|
+
concurrency: normalizeConcurrency(options.concurrency),
|
|
132
|
+
excludedPatterns,
|
|
133
|
+
});
|
|
134
|
+
const finalSummary = runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
|
|
135
|
+
if (finalSummary.failedCount > 0) {
|
|
136
|
+
throw new Error(`Upload finished with ${summary.failedCount} failures`);
|
|
194
137
|
}
|
|
195
138
|
|
|
196
|
-
|
|
197
|
-
|
|
139
|
+
return finalSummary;
|
|
198
140
|
}
|
|
199
141
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
module.exports = main
|
|
142
|
+
module.exports = main
|
package/index.js
CHANGED
|
@@ -1,41 +1,102 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
const { magentaBright } = require(
|
|
3
|
-
const figlet = require(
|
|
4
|
-
const clear = require(
|
|
5
|
-
const { program } = require(
|
|
6
|
-
const
|
|
2
|
+
const { magentaBright } = require("chalk")
|
|
3
|
+
const figlet = require("figlet")
|
|
4
|
+
const clear = require("clear")
|
|
5
|
+
const { program } = require("commander")
|
|
6
|
+
const command = require("./command")
|
|
7
|
+
const packageJson = require("./package.json")
|
|
8
|
+
const { shouldUseJson, printError, printJson } = require("./lib/output")
|
|
9
|
+
const { resolveLang, getLocale } = require("./lib/i18n")
|
|
7
10
|
|
|
11
|
+
const lang = resolveLang(process.argv)
|
|
12
|
+
const locale = getLocale(lang)
|
|
13
|
+
const isJsonMode = process.argv.includes("--json")
|
|
8
14
|
|
|
9
15
|
// 清除命令行
|
|
10
|
-
|
|
16
|
+
if (!shouldUseJson({ json: isJsonMode })) {
|
|
17
|
+
clear()
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
// 输出Logo
|
|
13
|
-
|
|
21
|
+
if (!isJsonMode) {
|
|
22
|
+
console.log(magentaBright(figlet.textSync("struggler-cli", { horizontalLayout: "full" })), "\n\n")
|
|
23
|
+
}
|
|
14
24
|
|
|
25
|
+
function formatItems(items, getLeft, getRight) {
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
return []
|
|
28
|
+
}
|
|
15
29
|
|
|
30
|
+
const width = items.reduce((max, item) => Math.max(max, getLeft(item).length), 0)
|
|
31
|
+
return items.map((item) => ` ${getLeft(item).padEnd(width)} ${getRight(item)}`.trimEnd())
|
|
32
|
+
}
|
|
16
33
|
|
|
17
|
-
program
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
program.configureHelp({
|
|
35
|
+
formatHelp: (cmd, helper) => {
|
|
36
|
+
const lines = []
|
|
37
|
+
lines.push(`${locale.help.usage}: ${helper.commandUsage(cmd)}`)
|
|
38
|
+
|
|
39
|
+
const description = helper.commandDescription(cmd)
|
|
40
|
+
if (description) {
|
|
41
|
+
lines.push("")
|
|
42
|
+
lines.push(`${locale.help.commandDescription} ${description}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const options = helper.visibleOptions(cmd)
|
|
46
|
+
if (options.length > 0) {
|
|
47
|
+
lines.push("")
|
|
48
|
+
lines.push(`${locale.help.options}:`)
|
|
49
|
+
lines.push(...formatItems(options, (option) => helper.optionTerm(option), (option) => option.description))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const commands = helper.visibleCommands(cmd)
|
|
53
|
+
if (commands.length > 0) {
|
|
54
|
+
lines.push("")
|
|
55
|
+
lines.push(`${locale.help.commands}:`)
|
|
56
|
+
lines.push(...formatItems(commands, (subcommand) => helper.subcommandTerm(subcommand), (subcommand) => subcommand.description()))
|
|
57
|
+
}
|
|
23
58
|
|
|
59
|
+
return lines.join("\n")
|
|
60
|
+
},
|
|
61
|
+
})
|
|
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)
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command("init")
|
|
67
|
+
.description(locale.commands.init)
|
|
68
|
+
.action(async () => {
|
|
69
|
+
await command.init(program.opts())
|
|
70
|
+
})
|
|
24
71
|
|
|
25
72
|
program
|
|
26
|
-
.command(
|
|
27
|
-
.description(
|
|
28
|
-
.action(()=>{
|
|
73
|
+
.command("upload")
|
|
74
|
+
.description(locale.commands.upload)
|
|
75
|
+
.action(async () => {
|
|
76
|
+
await command.upload(program.opts())
|
|
77
|
+
})
|
|
29
78
|
|
|
30
79
|
program
|
|
31
|
-
.command(
|
|
32
|
-
.description(
|
|
33
|
-
|
|
80
|
+
.command("refresh")
|
|
81
|
+
.description(locale.commands.refresh)
|
|
82
|
+
.action(async () => {
|
|
83
|
+
await command.refresh(program.opts())
|
|
84
|
+
})
|
|
34
85
|
|
|
35
86
|
program
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
.command("deploy")
|
|
88
|
+
.description(locale.commands.deploy)
|
|
89
|
+
.action(async () => {
|
|
90
|
+
await command.deploy(program.opts())
|
|
91
|
+
})
|
|
39
92
|
|
|
40
|
-
program.
|
|
93
|
+
program.addHelpCommand(true, locale.help.helpCommandDescription)
|
|
41
94
|
|
|
95
|
+
program.parseAsync().catch(error => {
|
|
96
|
+
if (isJsonMode) {
|
|
97
|
+
printJson({ ok: false, error: error.message || String(error) })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
printError({}, error.message || error)
|
|
101
|
+
process.exitCode = 1
|
|
102
|
+
})
|
package/lib/config.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
let fs = require('fs');
|
|
2
1
|
let path = require('path')
|
|
3
2
|
|
|
3
|
+
const DEFAULT_QINIU_CONFIG = './command/qiniu.json';
|
|
4
|
+
const DEFAULT_CONFIG = './command/config.json';
|
|
5
|
+
|
|
6
|
+
function resolveFromCwd(targetPath) {
|
|
7
|
+
return path.resolve(process.cwd(), targetPath);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getQiniuConfigPath(options = {}) {
|
|
11
|
+
return resolveFromCwd(options.config || DEFAULT_QINIU_CONFIG);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getConfigPath(options = {}) {
|
|
15
|
+
if (!options.config) {
|
|
16
|
+
return resolveFromCwd(DEFAULT_CONFIG);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const qiniuConfigPath = getQiniuConfigPath(options);
|
|
20
|
+
return path.join(path.dirname(qiniuConfigPath), 'config.json');
|
|
21
|
+
}
|
|
4
22
|
|
|
5
23
|
module.exports = {
|
|
6
24
|
getConfig: (options) => {
|
|
7
|
-
return
|
|
25
|
+
return getConfigPath(options)
|
|
8
26
|
},
|
|
9
27
|
getQiniuConfig: (options) => {
|
|
10
|
-
return
|
|
28
|
+
return getQiniuConfigPath(options)
|
|
11
29
|
},
|
|
12
30
|
getDir: (options) => {
|
|
13
|
-
return
|
|
31
|
+
return resolveFromCwd(options.dir || './dist')
|
|
14
32
|
},
|
|
15
33
|
};
|
package/lib/date.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function formatDate(date) {
|
|
2
|
+
const value = date || new Date();
|
|
3
|
+
const year = value.getFullYear();
|
|
4
|
+
const month = value.getMonth() + 1;
|
|
5
|
+
const day = value.getDate();
|
|
6
|
+
const hours = value.getHours();
|
|
7
|
+
const minutes = value.getMinutes();
|
|
8
|
+
|
|
9
|
+
return `${year}${month < 10 ? `0${month}` : month}${day < 10 ? `0${day}` : day}${hours < 10 ? `0${hours}` : hours}${minutes < 10 ? `0${minutes}` : minutes}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
formatDate,
|
|
14
|
+
};
|
package/lib/deploy.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { listFiles } = require('./files');
|
|
3
|
+
const { createIgnoreMatcher } = require('./ignore');
|
|
4
|
+
const { printMessage, writeManifest, shouldUseJson, printJson } = require('./output');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONCURRENCY = 5;
|
|
7
|
+
|
|
8
|
+
function normalizeConcurrency(value) {
|
|
9
|
+
const parsed = parseInt(value, 10);
|
|
10
|
+
if (Number.isNaN(parsed) || parsed < 1) {
|
|
11
|
+
return DEFAULT_CONCURRENCY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toRemoteKey(prefix, rootDir, localFile) {
|
|
18
|
+
return `${prefix}${path.relative(rootDir, localFile)}`.replace(/\\/g, '/');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function collectDeployFiles(dir, options = {}) {
|
|
22
|
+
const files = await listFiles(dir);
|
|
23
|
+
const ignoreMatcher = createIgnoreMatcher(dir, options);
|
|
24
|
+
return files
|
|
25
|
+
.filter((filePath) => !filePath.endsWith('.gz'))
|
|
26
|
+
.filter((filePath) => !ignoreMatcher.shouldIgnore(filePath))
|
|
27
|
+
.sort((left, right) => left.localeCompare(right));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
31
|
+
const normalizedConcurrency = Math.min(normalizeConcurrency(concurrency), Math.max(items.length, 1));
|
|
32
|
+
const results = new Array(items.length);
|
|
33
|
+
let nextIndex = 0;
|
|
34
|
+
|
|
35
|
+
async function consume() {
|
|
36
|
+
while (nextIndex < items.length) {
|
|
37
|
+
const currentIndex = nextIndex;
|
|
38
|
+
nextIndex += 1;
|
|
39
|
+
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tasks = Array.from({ length: normalizedConcurrency }, () => consume());
|
|
44
|
+
await Promise.all(tasks);
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createSummary(action, dryRun, results, startedAt, extra = {}) {
|
|
49
|
+
const succeeded = results.filter((item) => item.ok);
|
|
50
|
+
const failed = results.filter((item) => !item.ok);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
action,
|
|
54
|
+
dryRun,
|
|
55
|
+
total: results.length,
|
|
56
|
+
succeededCount: succeeded.length,
|
|
57
|
+
failedCount: failed.length,
|
|
58
|
+
durationMs: Date.now() - startedAt,
|
|
59
|
+
succeeded,
|
|
60
|
+
failed,
|
|
61
|
+
results,
|
|
62
|
+
...extra,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function logPlan(action, items, options) {
|
|
67
|
+
printMessage(options, `[dry-run] ${action} plan (${items.length} files)`);
|
|
68
|
+
items.forEach((item) => {
|
|
69
|
+
printMessage(options, `- ${item.localFile} -> ${item.target}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logSummary(summary, options) {
|
|
74
|
+
const mode = summary.dryRun ? 'dry-run' : 'live';
|
|
75
|
+
printMessage(options, `[summary] ${summary.action} mode=${mode} total=${summary.total} succeeded=${summary.succeededCount} failed=${summary.failedCount} duration=${summary.durationMs}ms`);
|
|
76
|
+
if (summary.failedCount > 0) {
|
|
77
|
+
summary.failed.forEach((item) => {
|
|
78
|
+
printMessage(options, `[failed] ${item.localFile}: ${item.error}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function maybeWriteManifest(summary, options, extra = {}) {
|
|
84
|
+
if (!options.manifest) {
|
|
85
|
+
return summary;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const manifestPath = writeManifest(options.manifest, {
|
|
89
|
+
...summary,
|
|
90
|
+
...extra,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...summary,
|
|
95
|
+
manifestPath,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function finalizeOutput(summary, options, extra = {}) {
|
|
100
|
+
const finalSummary = maybeWriteManifest(summary, options, extra);
|
|
101
|
+
if (shouldUseJson(options)) {
|
|
102
|
+
printJson(finalSummary);
|
|
103
|
+
} else {
|
|
104
|
+
logSummary(finalSummary, options);
|
|
105
|
+
if (finalSummary.manifestPath) {
|
|
106
|
+
printMessage(options, `[manifest] ${finalSummary.manifestPath}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return finalSummary;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureRequiredConfig(config, requiredFields) {
|
|
114
|
+
const missingFields = requiredFields.filter((field) => !config[field]);
|
|
115
|
+
if (missingFields.length > 0) {
|
|
116
|
+
throw new Error(`Missing required config: ${missingFields.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
DEFAULT_CONCURRENCY,
|
|
122
|
+
normalizeConcurrency,
|
|
123
|
+
toRemoteKey,
|
|
124
|
+
collectDeployFiles,
|
|
125
|
+
runWithConcurrency,
|
|
126
|
+
createSummary,
|
|
127
|
+
logPlan,
|
|
128
|
+
logSummary,
|
|
129
|
+
maybeWriteManifest,
|
|
130
|
+
finalizeOutput,
|
|
131
|
+
ensureRequiredConfig,
|
|
132
|
+
};
|
package/lib/files.js
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const fsp = fs.promises;
|
|
6
|
+
|
|
7
|
+
async function walkFiles(filePath) {
|
|
8
|
+
const entries = await fsp.readdir(filePath, { withFileTypes: true });
|
|
9
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
10
|
+
const absolutePath = path.join(filePath, entry.name);
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
return walkFiles(absolutePath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return [absolutePath];
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
return files.flat();
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
module.exports = {
|
|
7
22
|
// 获取目录名称
|
|
@@ -23,11 +38,10 @@ module.exports = {
|
|
|
23
38
|
|
|
24
39
|
setJsonData: (filePath,data) => {
|
|
25
40
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
26
|
-
fs.
|
|
41
|
+
fs.writeFileSync(
|
|
27
42
|
filePath,
|
|
28
43
|
JSON.stringify(data, null, "\t"
|
|
29
|
-
)
|
|
30
|
-
(err) => { }
|
|
44
|
+
)
|
|
31
45
|
)
|
|
32
46
|
},
|
|
33
47
|
setSyncJsonData: (filePath, data) => {
|
|
@@ -36,5 +50,17 @@ module.exports = {
|
|
|
36
50
|
filePath,
|
|
37
51
|
JSON.stringify(data, null, "\t"),
|
|
38
52
|
)
|
|
53
|
+
},
|
|
54
|
+
listFiles: async (filePath) => {
|
|
55
|
+
if (!fs.existsSync(filePath)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stat = await fsp.stat(filePath);
|
|
60
|
+
if (!stat.isDirectory()) {
|
|
61
|
+
return [filePath];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return walkFiles(filePath);
|
|
39
65
|
}
|
|
40
|
-
};
|
|
66
|
+
};
|