@struggler/cli 1.0.3 → 1.0.6

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 CHANGED
@@ -1,203 +1,181 @@
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
- let { getQiniuConfig, getDir } = require('../lib/config')
10
- let { getJsonData, setJsonData } = require('../lib/files')
5
+ let { getQiniuConfig, getDir, getCachePath } = require('../lib/config')
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
+ const { computeFileMd5, readCache, writeCache, isCacheHit, updateCacheEntry } = require('../lib/cache');
20
+
21
+ async function main(options, runtime = {}) {
22
+ const qiniuConfig = getJsonData(getQiniuConfig(options))
23
+ const prefix = runtime.prefix || qiniuPrefix.prefix(options);
24
+ let dir = getDir(options)
25
+ const startedAt = Date.now();
26
+ const ignoreMatcher = createIgnoreMatcher(dir, options);
27
+ const excludedPatterns = runtime.excludePatterns || ignoreMatcher.patterns;
28
+ const files = runtime.files || await collectDeployFiles(dir, options);
29
+
30
+ const useCache = !options.noCache;
31
+ const cachePath = getCachePath(options);
32
+ const cache = useCache ? readCache(cachePath) : {};
33
+
34
+ const plans = [];
35
+ const skippedPlans = [];
36
+
37
+ for (const localFile of files) {
38
+ const key = toRemoteKey(prefix, dir, localFile);
39
+ const plan = {
40
+ localFile,
41
+ key,
42
+ target: `${qiniuConfig.domain || ''}${key}`,
43
+ };
11
44
 
45
+ if (useCache && !options.dryRun) {
46
+ const md5 = computeFileMd5(localFile);
47
+ plan.localMd5 = md5;
48
+ if (isCacheHit(cache, key, md5)) {
49
+ skippedPlans.push(plan);
50
+ continue;
51
+ }
52
+ }
12
53
 
13
- function main(options) {
14
- const qiniuConfig = getJsonData(getQiniuConfig(options))
54
+ plans.push(plan);
55
+ }
56
+
57
+ if (options.dryRun) {
58
+ if (!runtime.suppressOutput) {
59
+ logPlan('upload', [...plans, ...skippedPlans], options);
60
+ }
61
+ const summary = createSummary('upload', true, [...plans, ...skippedPlans].map((plan) => ({
62
+ ok: true,
63
+ localFile: plan.localFile,
64
+ key: plan.key,
65
+ target: plan.target,
66
+ })), startedAt, {
67
+ prefix,
68
+ concurrency: normalizeConcurrency(options.concurrency),
69
+ excludedPatterns,
70
+ skippedCount: 0,
71
+ });
72
+ return runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
73
+ }
15
74
 
16
- //自己七牛云的秘钥
75
+ ensureRequiredConfig(
76
+ { ...qiniuConfig, 'publicPath(config.json)': prefix },
77
+ ['accessKey', 'secretKey', 'Bucket', 'zone', 'domain', 'publicPath(config.json)']
78
+ );
17
79
 
18
80
  var accessKey = qiniuConfig.accessKey
19
-
20
81
  var secretKey = qiniuConfig.secretKey;
21
-
22
82
  var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
23
-
24
83
  var config = new qiniu.conf.Config();
25
-
26
- // 空间对应的机房,zone_z1代表华北,其他配置参见七牛云文档
27
-
28
84
  config.zone = qiniu.zone[qiniuConfig.zone];
29
-
30
- // 是否使用https域名
31
-
32
85
  config.useHttpsDomain = true;
33
-
34
- // 上传是否使用cdn加速
35
-
36
86
  config.useCdnDomain = true;
37
87
 
38
88
  var formUploader = new qiniu.form_up.FormUploader(config);
39
-
40
89
  var putExtra = new qiniu.form_up.PutExtra();
41
90
 
42
- //文件前缀
43
-
44
- const prefix = qiniuPrefix.prefix(options);
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
-
91
+ function upload(plan, attempt = 1) {
92
+ var uploadOptions = {
93
+ scope: `${qiniuConfig.Bucket}:${plan.key}`
93
94
  };
94
-
95
- var putPolicy = new qiniu.rs.PutPolicy(options);
96
-
95
+ var putPolicy = new qiniu.rs.PutPolicy(uploadOptions);
97
96
  var uploadToken = putPolicy.uploadToken(mac);
98
97
 
99
- formUploader.putFile(uploadToken, key, localFile, null, function (respErr,
100
-
101
- respBody, respInfo) {
102
-
103
- if (respErr) {
104
-
105
- // console.log(uploadToken);
106
-
107
- console.log(localFile + "文件上传失败,正在重新上传-----------");
108
-
109
- // console.log(respInfo.statusCode);
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
-
98
+ return new Promise((resolve, reject) => {
99
+ formUploader.putFile(uploadToken, plan.key, plan.localFile, putExtra, async function (respErr, respBody, respInfo) {
100
+ if (respErr || respInfo.statusCode !== 200) {
101
+ if (attempt < 3) {
102
+ console.log(`${plan.localFile} 上传失败,正在进行第 ${attempt + 1} 次重试`);
103
+ try {
104
+ const retryResult = await upload(plan, attempt + 1);
105
+ resolve(retryResult);
106
+ } catch (retryError) {
107
+ reject(retryError);
108
+ }
109
+ return;
134
110
  }
135
111
 
112
+ const errorMessage = respErr || (respBody && respBody.error) || `statusCode=${respInfo && respInfo.statusCode}`;
113
+ reject(new Error(errorMessage));
114
+ return;
136
115
  }
137
116
 
138
- }
139
-
117
+ resolve({
118
+ hash: respBody.hash,
119
+ key: plan.key,
120
+ target: plan.target,
121
+ });
122
+ });
140
123
  });
141
-
142
124
  }
143
125
 
144
- //遍历文件夹
145
-
146
- function displayFile(param) {
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
-
126
+ const results = await runWithConcurrency(plans, normalizeConcurrency(options.concurrency), async (plan) => {
127
+ try {
128
+ const response = await upload(plan);
129
+ if (!runtime.suppressOutput) {
130
+ printMessage(options, `[uploaded] ${plan.localFile} -> ${plan.target}`);
190
131
  }
191
-
192
- })
193
-
132
+ if (useCache && plan.localMd5) {
133
+ updateCacheEntry(cache, plan.key, plan.localMd5, response.hash);
134
+ }
135
+ return {
136
+ ok: true,
137
+ localFile: plan.localFile,
138
+ key: response.key,
139
+ target: response.target,
140
+ hash: response.hash,
141
+ };
142
+ } catch (error) {
143
+ return {
144
+ ok: false,
145
+ localFile: plan.localFile,
146
+ key: plan.key,
147
+ target: plan.target,
148
+ error: error.message,
149
+ };
150
+ }
151
+ });
152
+
153
+ if (useCache) {
154
+ writeCache(cachePath, cache);
194
155
  }
195
156
 
196
- displayFile(dir);
157
+ const skippedResults = skippedPlans.map((plan) => ({
158
+ ok: true,
159
+ skipped: true,
160
+ localFile: plan.localFile,
161
+ key: plan.key,
162
+ target: plan.target,
163
+ }));
164
+
165
+ const allResults = [...results, ...skippedResults];
166
+
167
+ const summary = createSummary('upload', false, allResults, startedAt, {
168
+ prefix,
169
+ concurrency: normalizeConcurrency(options.concurrency),
170
+ excludedPatterns,
171
+ skippedCount: skippedPlans.length,
172
+ });
173
+ const finalSummary = runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
174
+ if (finalSummary.failedCount > 0) {
175
+ throw new Error(`Upload finished with ${summary.failedCount} failures`);
176
+ }
197
177
 
178
+ return finalSummary;
198
179
  }
199
180
 
200
-
201
-
202
-
203
- module.exports = main
181
+ module.exports = main
package/index.js CHANGED
@@ -1,48 +1,102 @@
1
1
  #! /usr/bin/env node
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")
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
- clear();
16
+ if (!shouldUseJson({ json: isJsonMode })) {
17
+ clear()
18
+ }
11
19
 
12
20
  // 输出Logo
13
- console.log(magentaBright(figlet.textSync('struggler-cli', { horizontalLayout: 'full' })),'\n\n');
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
- .name('struggler-cli')
19
- .description('CLI to Upload vite packaged files to Qiniu Cloud OSS.')
20
- .version('1.0.3')
21
- .option('-c, --config <path>', 'Specify the path to upload configuration file.', './command/qiniu.json')
22
- .option('-d, --dir <path>', 'Specify the dir to upload.', './dist')
34
+ program.configureHelp({
35
+ formatHelp: (cmd, helper) => {
36
+ const lines = []
37
+ lines.push(`${locale.help.usage}: ${helper.commandUsage(cmd)}`)
23
38
 
39
+ const description = helper.commandDescription(cmd)
40
+ if (description) {
41
+ lines.push("")
42
+ lines.push(`${locale.help.commandDescription} ${description}`)
43
+ }
24
44
 
25
- program
26
- .command('init')
27
- .description('Create upload configuration for specified path.')
28
- .action(()=>{command.init(program.opts())})
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
+ }
29
51
 
30
- program
31
- .command('upload')
32
- .description('Upload files under the specified file to Qiniu Cloud.')
33
- .action(() => { command.upload(program.opts()) })
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
+ }
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("--no-cache", locale.options.noCache).option("--lang <lang>", locale.options.lang, lang)
34
64
 
35
65
  program
36
- .command('refresh')
37
- .description('refresh Qiniu Cloud files Cdn')
38
- .action(() => { command.refresh(program.opts()) })
66
+ .command("init")
67
+ .description(locale.commands.init)
68
+ .action(async () => {
69
+ await command.init(program.opts())
70
+ })
39
71
 
72
+ program
73
+ .command("upload")
74
+ .description(locale.commands.upload)
75
+ .action(async () => {
76
+ await command.upload(program.opts())
77
+ })
40
78
 
79
+ program
80
+ .command("refresh")
81
+ .description(locale.commands.refresh)
82
+ .action(async () => {
83
+ await command.refresh(program.opts())
84
+ })
41
85
 
42
86
  program
43
- .command('addVersion')
44
- .description('Package add version.')
45
- .action(() => { command.addVersion(program.opts()) })
87
+ .command("deploy")
88
+ .description(locale.commands.deploy)
89
+ .action(async () => {
90
+ await command.deploy(program.opts())
91
+ })
46
92
 
47
- program.parse()
93
+ program.addHelpCommand(true, locale.help.helpCommandDescription)
48
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/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
@@ -1,15 +1,46 @@
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
+ const DEFAULT_CACHE = './command/upload-cache.json';
6
+
7
+ function resolveFromCwd(targetPath) {
8
+ return path.resolve(process.cwd(), targetPath);
9
+ }
10
+
11
+ function getQiniuConfigPath(options = {}) {
12
+ return resolveFromCwd(options.config || DEFAULT_QINIU_CONFIG);
13
+ }
14
+
15
+ function getConfigPath(options = {}) {
16
+ if (!options.config) {
17
+ return resolveFromCwd(DEFAULT_CONFIG);
18
+ }
19
+
20
+ const qiniuConfigPath = getQiniuConfigPath(options);
21
+ return path.join(path.dirname(qiniuConfigPath), 'config.json');
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
+ }
4
32
 
5
33
  module.exports = {
6
34
  getConfig: (options) => {
7
- return path.resolve(process.cwd(), './command/config.json')
35
+ return getConfigPath(options)
8
36
  },
9
37
  getQiniuConfig: (options) => {
10
- return path.resolve(process.cwd(), options.config || './command/qiniu.json')
38
+ return getQiniuConfigPath(options)
39
+ },
40
+ getCachePath: (options) => {
41
+ return getCachePath(options)
11
42
  },
12
43
  getDir: (options) => {
13
- return path.resolve(process.cwd(), options.dir || './dist')
44
+ return resolveFromCwd(options.dir || './dist')
14
45
  },
15
46
  };
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
+ };