hw-weapp-compiler 1.0.23 → 1.0.24
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/build/utils/upload.js +281 -52
- package/package.json +3 -2
- package/test/upload.js +92 -0
package/README.md
CHANGED
package/build/utils/upload.js
CHANGED
|
@@ -16,15 +16,198 @@ const { DIST_DIR } = require('../config/constants');
|
|
|
16
16
|
|
|
17
17
|
const { obsConfig, ossConfig } = getConfig();
|
|
18
18
|
|
|
19
|
+
const UPLOAD_CONCURRENCY = 10;
|
|
20
|
+
const DEFAULT_OBS_TIMEOUT_SECONDS = 15;
|
|
21
|
+
const DEFAULT_OBS_MAX_RETRY_COUNT = 1;
|
|
22
|
+
const OBS_PENDING_WARNING_MS = 15 * 1000;
|
|
23
|
+
const OBS_TIMEOUT_BUFFER_MS = 5 * 1000;
|
|
24
|
+
|
|
19
25
|
let obsClient;
|
|
20
26
|
let ossClient;
|
|
21
27
|
let progress;
|
|
22
28
|
let uploadQueue = {};
|
|
29
|
+
let uploadRunner;
|
|
30
|
+
|
|
31
|
+
function getRemoteKey(config, file) {
|
|
32
|
+
return compatiblePath(path.join(config.dir, path.relative(DIST_DIR, file)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getDisplayFile(file) {
|
|
36
|
+
return compatiblePath(path.relative(process.cwd(), file));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getObsStatus(value) {
|
|
40
|
+
return value && value.CommonMsg ? value.CommonMsg.Status : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getInvalidObsResponseError(operation) {
|
|
44
|
+
const error = new Error(`OBS ${operation} returned no HTTP status`);
|
|
45
|
+
error.code = 'OBS_INVALID_RESPONSE';
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatObsError(error) {
|
|
50
|
+
if (!error) {
|
|
51
|
+
return 'unknown error';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const commonMsg = error.CommonMsg || {};
|
|
55
|
+
const details = {
|
|
56
|
+
status: commonMsg.Status || error.statusCode,
|
|
57
|
+
code: commonMsg.Code || error.code,
|
|
58
|
+
message: commonMsg.Message || error.message,
|
|
59
|
+
requestId: commonMsg.RequestId || error.requestId,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = Object.entries(details)
|
|
63
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
64
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
65
|
+
.join(', ');
|
|
66
|
+
|
|
67
|
+
return result || String(error);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toNonNegativeNumber(value, fallback) {
|
|
71
|
+
const number = Number(value);
|
|
72
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toPositiveNumber(value, fallback) {
|
|
76
|
+
const number = Number(value);
|
|
77
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getObsRuntimeOptions() {
|
|
81
|
+
const timeoutSeconds = toPositiveNumber(
|
|
82
|
+
obsConfig.timeout,
|
|
83
|
+
DEFAULT_OBS_TIMEOUT_SECONDS,
|
|
84
|
+
);
|
|
85
|
+
const maxRetryCount = toNonNegativeNumber(
|
|
86
|
+
obsConfig.max_retry_count,
|
|
87
|
+
DEFAULT_OBS_MAX_RETRY_COUNT,
|
|
88
|
+
);
|
|
89
|
+
const defaultOperationTimeoutMs =
|
|
90
|
+
timeoutSeconds * (maxRetryCount + 1) * 1000 + OBS_TIMEOUT_BUFFER_MS;
|
|
91
|
+
const operationTimeoutMs = obsConfig.operation_timeout
|
|
92
|
+
? toPositiveNumber(
|
|
93
|
+
obsConfig.operation_timeout,
|
|
94
|
+
defaultOperationTimeoutMs / 1000,
|
|
95
|
+
) * 1000
|
|
96
|
+
: defaultOperationTimeoutMs;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
timeoutSeconds,
|
|
100
|
+
maxRetryCount,
|
|
101
|
+
operationTimeoutMs,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createObsRequestLogger(operation, file, key) {
|
|
106
|
+
const startedAt = Date.now();
|
|
107
|
+
const { timeoutSeconds, maxRetryCount, operationTimeoutMs } = getObsRuntimeOptions();
|
|
108
|
+
const displayFile = getDisplayFile(file);
|
|
109
|
+
|
|
110
|
+
console.log(`[OBS] ${operation} started: key=${key}, file=${displayFile}`);
|
|
111
|
+
|
|
112
|
+
const pendingTimer = setTimeout(() => {
|
|
113
|
+
console.warn(
|
|
114
|
+
`[OBS] ${operation} is still pending after ${OBS_PENDING_WARNING_MS / 1000}s: key=${key}, ` +
|
|
115
|
+
`server=${obsConfig.server}, bucket=${obsConfig.bucket}, ` +
|
|
116
|
+
`sdkTimeout=${timeoutSeconds}s, maxRetryCount=${maxRetryCount}, ` +
|
|
117
|
+
`operationTimeout=${operationTimeoutMs / 1000}s`,
|
|
118
|
+
);
|
|
119
|
+
}, OBS_PENDING_WARNING_MS);
|
|
120
|
+
|
|
121
|
+
// 诊断定时器不应单独阻止构建进程退出。
|
|
122
|
+
if (pendingTimer.unref) {
|
|
123
|
+
pendingTimer.unref();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
finish(outcome, detail) {
|
|
128
|
+
clearTimeout(pendingTimer);
|
|
129
|
+
const elapsed = Date.now() - startedAt;
|
|
130
|
+
const suffix = detail ? `, ${detail}` : '';
|
|
131
|
+
const message = `[OBS] ${operation} ${outcome} in ${elapsed}ms: key=${key}${suffix}`;
|
|
132
|
+
|
|
133
|
+
if (outcome === 'failed') {
|
|
134
|
+
console.error(message);
|
|
135
|
+
} else if (outcome === 'miss') {
|
|
136
|
+
console.warn(message);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(message);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runObsRequest(operation, file, key, request) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const requestLogger = createObsRequestLogger(operation, file, key);
|
|
147
|
+
const { operationTimeoutMs } = getObsRuntimeOptions();
|
|
148
|
+
let settled = false;
|
|
149
|
+
let operationTimer;
|
|
150
|
+
let activeRequest;
|
|
151
|
+
|
|
152
|
+
const finish = (error, result, outcome) => {
|
|
153
|
+
if (settled) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
settled = true;
|
|
158
|
+
clearTimeout(operationTimer);
|
|
159
|
+
activeRequest = null;
|
|
160
|
+
requestLogger.finish(
|
|
161
|
+
outcome || (error ? 'failed' : 'completed'),
|
|
162
|
+
formatObsError(error || result),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (error) {
|
|
166
|
+
reject(error);
|
|
167
|
+
} else {
|
|
168
|
+
resolve(result);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const armOperationTimeout = () => {
|
|
173
|
+
if (settled) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
clearTimeout(operationTimer);
|
|
177
|
+
operationTimer = setTimeout(() => {
|
|
178
|
+
const requestToAbort = activeRequest;
|
|
179
|
+
const error = new Error(
|
|
180
|
+
`OBS ${operation} timed out after ${operationTimeoutMs / 1000}s without progress`,
|
|
181
|
+
);
|
|
182
|
+
error.code = 'OBS_OPERATION_TIMEOUT';
|
|
183
|
+
finish(error);
|
|
184
|
+
if (requestToAbort && requestToAbort.destroy) {
|
|
185
|
+
requestToAbort.destroy();
|
|
186
|
+
}
|
|
187
|
+
}, operationTimeoutMs);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const registerRequest = (clientRequest) => {
|
|
191
|
+
activeRequest = clientRequest;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
armOperationTimeout();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
request(finish, armOperationTimeout, registerRequest);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
finish(error);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
23
203
|
|
|
24
204
|
function getObsClient() {
|
|
25
205
|
if (!obsClient) {
|
|
206
|
+
const { timeoutSeconds, maxRetryCount } = getObsRuntimeOptions();
|
|
26
207
|
obsClient = new OBSClient({
|
|
27
208
|
...obsConfig,
|
|
209
|
+
timeout: timeoutSeconds,
|
|
210
|
+
max_retry_count: maxRetryCount,
|
|
28
211
|
});
|
|
29
212
|
}
|
|
30
213
|
return obsClient;
|
|
@@ -41,49 +224,60 @@ function getOssClient() {
|
|
|
41
224
|
|
|
42
225
|
function doOssUpload(file) {
|
|
43
226
|
return getOssClient().put(
|
|
44
|
-
|
|
227
|
+
getRemoteKey(ossConfig, file),
|
|
45
228
|
file,
|
|
46
229
|
);
|
|
47
230
|
}
|
|
48
231
|
|
|
49
232
|
function getOssStat(file) {
|
|
50
|
-
return getOssClient().head(
|
|
233
|
+
return getOssClient().head(getRemoteKey(ossConfig, file));
|
|
51
234
|
}
|
|
52
235
|
|
|
53
236
|
function doObsUpload(file) {
|
|
54
|
-
|
|
237
|
+
const key = getRemoteKey(obsConfig, file);
|
|
238
|
+
return runObsRequest('upload', file, key, (finish, reportProgress, registerRequest) => {
|
|
55
239
|
getObsClient().putObject(
|
|
56
240
|
{
|
|
57
241
|
Bucket: obsConfig.bucket,
|
|
58
|
-
Key:
|
|
242
|
+
Key: key,
|
|
59
243
|
SourceFile: file,
|
|
244
|
+
ProgressCallback: reportProgress,
|
|
245
|
+
RequestHook: registerRequest,
|
|
60
246
|
},
|
|
61
247
|
(err, result) => {
|
|
248
|
+
const status = getObsStatus(result);
|
|
62
249
|
if (err) {
|
|
63
|
-
|
|
64
|
-
} else if (
|
|
65
|
-
|
|
250
|
+
finish(err);
|
|
251
|
+
} else if (status === undefined) {
|
|
252
|
+
finish(getInvalidObsResponseError('upload'));
|
|
253
|
+
} else if (status >= 300) {
|
|
254
|
+
finish(result);
|
|
66
255
|
} else {
|
|
67
|
-
|
|
256
|
+
finish(null, result);
|
|
68
257
|
}
|
|
69
258
|
},
|
|
70
259
|
);
|
|
71
260
|
});
|
|
72
261
|
}
|
|
73
262
|
async function getObsStat(file) {
|
|
74
|
-
|
|
263
|
+
const key = getRemoteKey(obsConfig, file);
|
|
264
|
+
return runObsRequest('metadata check', file, key, (finish, _reportProgress, registerRequest) => {
|
|
75
265
|
getObsClient().getObjectMetadata(
|
|
76
266
|
{
|
|
77
267
|
Bucket: obsConfig.bucket,
|
|
78
|
-
Key:
|
|
268
|
+
Key: key,
|
|
269
|
+
RequestHook: registerRequest,
|
|
79
270
|
},
|
|
80
271
|
(err, result) => {
|
|
272
|
+
const status = getObsStatus(result);
|
|
81
273
|
if (err) {
|
|
82
|
-
|
|
83
|
-
} else if (
|
|
84
|
-
|
|
274
|
+
finish(err);
|
|
275
|
+
} else if (status === undefined) {
|
|
276
|
+
finish(getInvalidObsResponseError('metadata check'));
|
|
277
|
+
} else if (status < 300) {
|
|
278
|
+
finish(null, result);
|
|
85
279
|
} else {
|
|
86
|
-
|
|
280
|
+
finish(result, null, 'miss');
|
|
87
281
|
}
|
|
88
282
|
},
|
|
89
283
|
);
|
|
@@ -111,8 +305,7 @@ function doUpload(file) {
|
|
|
111
305
|
}
|
|
112
306
|
|
|
113
307
|
function updateProgress() {
|
|
114
|
-
const completed =
|
|
115
|
-
Object.entries(uploadQueue).filter((item) => item[1] === 'completed').length + 1;
|
|
308
|
+
const completed = Object.values(uploadQueue).filter((status) => status === 'completed').length;
|
|
116
309
|
const total = Object.keys(uploadQueue).length;
|
|
117
310
|
|
|
118
311
|
if (!progress) {
|
|
@@ -121,64 +314,100 @@ function updateProgress() {
|
|
|
121
314
|
width: 40,
|
|
122
315
|
clear: true,
|
|
123
316
|
});
|
|
317
|
+
} else {
|
|
318
|
+
// watch 构建期间可能继续追加资源,进度条总数需要同步扩容。
|
|
319
|
+
progress.total = total;
|
|
124
320
|
}
|
|
125
321
|
|
|
126
322
|
progress.tick();
|
|
127
323
|
|
|
128
|
-
if (completed === total) {
|
|
129
|
-
progress.tick({
|
|
130
|
-
current: total,
|
|
131
|
-
});
|
|
132
|
-
progress = null;
|
|
133
|
-
uploadQueue = {};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
324
|
return `[${completed}/${total}]`;
|
|
137
325
|
}
|
|
138
326
|
|
|
139
|
-
async function
|
|
140
|
-
|
|
141
|
-
.filter((item) => item[1] === false)
|
|
142
|
-
.splice(0, 10);
|
|
143
|
-
|
|
144
|
-
if (files && files.length) {
|
|
145
|
-
await Promise.all(
|
|
146
|
-
files.map(async (file) => {
|
|
147
|
-
uploadQueue[file[0]] = 'uploading';
|
|
148
|
-
try {
|
|
149
|
-
await getStat(file[0]);
|
|
150
|
-
setStorage(file[0], true);
|
|
151
|
-
updateProgress();
|
|
152
|
-
} catch (error) {
|
|
153
|
-
try {
|
|
154
|
-
await doUpload(file[0]);
|
|
155
|
-
setStorage(file[0], true);
|
|
156
|
-
} catch (uploadError) {
|
|
157
|
-
console.error(`Failed to upload ${file[0]}:`, uploadError);
|
|
158
|
-
}
|
|
159
|
-
updateProgress();
|
|
160
|
-
}
|
|
161
|
-
uploadQueue[file[0]] = 'completed';
|
|
162
|
-
}),
|
|
163
|
-
);
|
|
327
|
+
async function uploadFile(file) {
|
|
328
|
+
uploadQueue[file] = 'uploading';
|
|
164
329
|
|
|
165
|
-
|
|
330
|
+
try {
|
|
331
|
+
await getStat(file);
|
|
332
|
+
setStorage(file, true);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
try {
|
|
335
|
+
await doUpload(file);
|
|
336
|
+
setStorage(file, true);
|
|
337
|
+
} catch (uploadError) {
|
|
338
|
+
console.error(
|
|
339
|
+
`[assets] upload failed: file=${getDisplayFile(file)}, ${formatObsError(uploadError)}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
} finally {
|
|
343
|
+
uploadQueue[file] = 'completed';
|
|
344
|
+
updateProgress();
|
|
166
345
|
}
|
|
167
346
|
}
|
|
168
347
|
|
|
348
|
+
function checkUpload() {
|
|
349
|
+
// done hook 可能在上一次上传尚未结束时再次触发。复用同一个 runner,避免每次构建
|
|
350
|
+
// 都额外启动 10 个请求,最终耗尽 OBS/HTTP 连接池。
|
|
351
|
+
if (uploadRunner) {
|
|
352
|
+
return uploadRunner;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
uploadRunner = (async () => {
|
|
356
|
+
while (true) {
|
|
357
|
+
const files = Object.entries(uploadQueue)
|
|
358
|
+
.filter((item) => item[1] === 'pending')
|
|
359
|
+
.slice(0, UPLOAD_CONCURRENCY)
|
|
360
|
+
.map((item) => item[0]);
|
|
361
|
+
|
|
362
|
+
if (!files.length) {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await Promise.all(files.map(uploadFile));
|
|
367
|
+
}
|
|
368
|
+
})().finally(() => {
|
|
369
|
+
uploadRunner = null;
|
|
370
|
+
|
|
371
|
+
// 文件状态已经写完后再清空,避免旧实现最后一个文件把已清空的队列重新写回。
|
|
372
|
+
if (!Object.values(uploadQueue).includes('pending')) {
|
|
373
|
+
progress = null;
|
|
374
|
+
uploadQueue = {};
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 兜住 runner 完成与 watch 新增资源恰好交错的情况。
|
|
379
|
+
checkUpload();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return uploadRunner;
|
|
383
|
+
}
|
|
384
|
+
|
|
169
385
|
function addToUploadQueue(assets) {
|
|
170
386
|
if (!obsConfig && !ossConfig) {
|
|
171
387
|
console.warn('请配置obsConfig 或 ossConfig,否则无法上传文件到obs 或 oss');
|
|
172
388
|
return;
|
|
173
389
|
}
|
|
174
390
|
|
|
391
|
+
let cachedCount = 0;
|
|
392
|
+
let pendingCount = 0;
|
|
393
|
+
|
|
175
394
|
assets.forEach((asset) => {
|
|
176
395
|
const file = path.resolve(DIST_DIR, asset);
|
|
396
|
+
if (getStorage(file)) {
|
|
397
|
+
cachedCount += 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
177
400
|
if (uploadQueue[file] === undefined) {
|
|
178
|
-
uploadQueue[file] =
|
|
401
|
+
uploadQueue[file] = 'pending';
|
|
402
|
+
pendingCount += 1;
|
|
179
403
|
}
|
|
180
404
|
});
|
|
181
|
-
|
|
405
|
+
|
|
406
|
+
console.log(
|
|
407
|
+
`[assets] upload queue: provider=${ossConfig ? 'OSS' : 'OBS'}, total=${assets.length}, ` +
|
|
408
|
+
`cached=${cachedCount}, pending=${pendingCount}`,
|
|
409
|
+
);
|
|
410
|
+
return pendingCount ? checkUpload() : Promise.resolve();
|
|
182
411
|
}
|
|
183
412
|
|
|
184
413
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hw-weapp-compiler",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
4
4
|
"description": "基于 webpack5 的小程序构建工具,兼容 Node.js 14~24",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "node ./index.js build",
|
|
54
54
|
"dev": "nodemon --inspect --trace-deprecation --watch build ./index.js dev",
|
|
55
|
-
"simulation": "node ./index.js build -s"
|
|
55
|
+
"simulation": "node ./index.js build -s",
|
|
56
|
+
"test": "node ./test/upload.js"
|
|
56
57
|
}
|
|
57
58
|
}
|
package/test/upload.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
async function run() {
|
|
8
|
+
const requests = [];
|
|
9
|
+
const sockets = new Set();
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
requests.push(`${req.method} ${req.url}`);
|
|
12
|
+
|
|
13
|
+
if (req.url.includes('stuck.png')) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (req.method === 'HEAD') {
|
|
18
|
+
res.statusCode = 404;
|
|
19
|
+
res.end();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
req.resume();
|
|
24
|
+
req.on('end', () => {
|
|
25
|
+
res.statusCode = 200;
|
|
26
|
+
res.setHeader('etag', 'test-etag');
|
|
27
|
+
res.end();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
server.on('connection', (socket) => {
|
|
31
|
+
sockets.add(socket);
|
|
32
|
+
socket.on('close', () => sockets.delete(socket));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
36
|
+
|
|
37
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hw-upload-test-'));
|
|
38
|
+
const originalCwd = process.cwd();
|
|
39
|
+
const endpoint = `http://127.0.0.1:${server.address().port}`;
|
|
40
|
+
const uploadModule = path.resolve(originalCwd, 'build/utils/upload.js');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(path.join(projectDir, 'dist/assets'), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/ok.png'), 'ok');
|
|
45
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/stuck.png'), 'stuck');
|
|
46
|
+
fs.writeFileSync(path.join(projectDir, 'dist/assets/later.png'), 'later');
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(projectDir, '.weapp.js'),
|
|
49
|
+
`module.exports = { obsConfig: ${JSON.stringify({
|
|
50
|
+
access_key_id: 'test-ak',
|
|
51
|
+
secret_access_key: 'test-sk',
|
|
52
|
+
server: endpoint,
|
|
53
|
+
bucket: 'test-bucket',
|
|
54
|
+
dir: 'test-dir',
|
|
55
|
+
timeout: 10,
|
|
56
|
+
max_retry_count: 0,
|
|
57
|
+
operation_timeout: 0.1,
|
|
58
|
+
})} };`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
process.chdir(projectDir);
|
|
62
|
+
const { addToUploadQueue } = require(uploadModule);
|
|
63
|
+
|
|
64
|
+
await addToUploadQueue(['assets/ok.png']);
|
|
65
|
+
assert(requests.some((request) => request.startsWith('HEAD ')));
|
|
66
|
+
assert(requests.some((request) => request.startsWith('PUT ')));
|
|
67
|
+
|
|
68
|
+
const startedAt = Date.now();
|
|
69
|
+
const stuckRun = addToUploadQueue(['assets/stuck.png']);
|
|
70
|
+
const sameRun = addToUploadQueue(['assets/later.png']);
|
|
71
|
+
assert.strictEqual(stuckRun, sameRun, 'watch rebuild should reuse the active queue runner');
|
|
72
|
+
await sameRun;
|
|
73
|
+
|
|
74
|
+
const elapsed = Date.now() - startedAt;
|
|
75
|
+
assert(elapsed < 2000, `timeout fallback took ${elapsed}ms`);
|
|
76
|
+
assert(requests.filter((request) => request.includes('stuck.png')).length >= 2);
|
|
77
|
+
assert(requests.some((request) => request.includes('later.png')));
|
|
78
|
+
|
|
79
|
+
// 等待 storage.js 的防抖写入结束后再清理测试目录。
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
81
|
+
} finally {
|
|
82
|
+
process.chdir(originalCwd);
|
|
83
|
+
sockets.forEach((socket) => socket.destroy());
|
|
84
|
+
await new Promise((resolve) => server.close(resolve));
|
|
85
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
run().catch((error) => {
|
|
90
|
+
console.error(error);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
});
|