hw-weapp-compiler 1.0.23 → 1.0.25

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 CHANGED
@@ -53,6 +53,10 @@ module.exports = {
53
53
  server: 'XXX',
54
54
  bucket: 'XXX',
55
55
  dir: 'weapp',
56
+ // 可选:单次 SDK 请求超时、重试次数,以及编译器无进展兜底超时(秒)
57
+ timeout: 15,
58
+ max_retry_count: 1,
59
+ operation_timeout: 35,
56
60
  },
57
61
  ossConfig: {
58
62
  region: '<Your region>',
@@ -16,18 +16,236 @@ 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;
26
+ let obsClientReady;
20
27
  let ossClient;
21
28
  let progress;
22
29
  let uploadQueue = {};
30
+ let uploadRunner;
31
+
32
+ function getRemoteKey(config, file) {
33
+ return compatiblePath(path.join(config.dir, path.relative(DIST_DIR, file)));
34
+ }
35
+
36
+ function getDisplayFile(file) {
37
+ return compatiblePath(path.relative(process.cwd(), file));
38
+ }
39
+
40
+ function getObsStatus(value) {
41
+ return value && value.CommonMsg ? value.CommonMsg.Status : undefined;
42
+ }
43
+
44
+ function getInvalidObsResponseError(operation) {
45
+ const error = new Error(`OBS ${operation} returned no HTTP status`);
46
+ error.code = 'OBS_INVALID_RESPONSE';
47
+ return error;
48
+ }
49
+
50
+ function formatObsError(error) {
51
+ if (!error) {
52
+ return 'unknown error';
53
+ }
54
+
55
+ const commonMsg = error.CommonMsg || {};
56
+ const details = {
57
+ status: commonMsg.Status || error.statusCode,
58
+ code: commonMsg.Code || error.code,
59
+ message: commonMsg.Message || error.message,
60
+ requestId: commonMsg.RequestId || error.requestId,
61
+ };
62
+
63
+ const result = Object.entries(details)
64
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
65
+ .map(([key, value]) => `${key}=${value}`)
66
+ .join(', ');
67
+
68
+ return result || String(error);
69
+ }
70
+
71
+ function toNonNegativeNumber(value, fallback) {
72
+ const number = Number(value);
73
+ return Number.isFinite(number) && number >= 0 ? number : fallback;
74
+ }
75
+
76
+ function toPositiveNumber(value, fallback) {
77
+ const number = Number(value);
78
+ return Number.isFinite(number) && number > 0 ? number : fallback;
79
+ }
80
+
81
+ function getObsRuntimeOptions() {
82
+ const timeoutSeconds = toPositiveNumber(
83
+ obsConfig.timeout,
84
+ DEFAULT_OBS_TIMEOUT_SECONDS,
85
+ );
86
+ const maxRetryCount = toNonNegativeNumber(
87
+ obsConfig.max_retry_count,
88
+ DEFAULT_OBS_MAX_RETRY_COUNT,
89
+ );
90
+ const defaultOperationTimeoutMs =
91
+ timeoutSeconds * (maxRetryCount + 1) * 1000 + OBS_TIMEOUT_BUFFER_MS;
92
+ const operationTimeoutMs = obsConfig.operation_timeout
93
+ ? toPositiveNumber(
94
+ obsConfig.operation_timeout,
95
+ defaultOperationTimeoutMs / 1000,
96
+ ) * 1000
97
+ : defaultOperationTimeoutMs;
98
+
99
+ return {
100
+ timeoutSeconds,
101
+ maxRetryCount,
102
+ operationTimeoutMs,
103
+ };
104
+ }
105
+
106
+ function createObsRequestLogger(operation, file, key) {
107
+ const startedAt = Date.now();
108
+ const { timeoutSeconds, maxRetryCount, operationTimeoutMs } = getObsRuntimeOptions();
109
+ const displayFile = getDisplayFile(file);
110
+
111
+ console.log(`[OBS] ${operation} started: key=${key}, file=${displayFile}`);
112
+
113
+ const pendingTimer = setTimeout(() => {
114
+ console.warn(
115
+ `[OBS] ${operation} is still pending after ${OBS_PENDING_WARNING_MS / 1000}s: key=${key}, ` +
116
+ `server=${obsConfig.server}, bucket=${obsConfig.bucket}, ` +
117
+ `sdkTimeout=${timeoutSeconds}s, maxRetryCount=${maxRetryCount}, ` +
118
+ `operationTimeout=${operationTimeoutMs / 1000}s`,
119
+ );
120
+ }, OBS_PENDING_WARNING_MS);
121
+
122
+ // 诊断定时器不应单独阻止构建进程退出。
123
+ if (pendingTimer.unref) {
124
+ pendingTimer.unref();
125
+ }
126
+
127
+ return {
128
+ finish(outcome, detail) {
129
+ clearTimeout(pendingTimer);
130
+ const elapsed = Date.now() - startedAt;
131
+ const suffix = detail ? `, ${detail}` : '';
132
+ const message = `[OBS] ${operation} ${outcome} in ${elapsed}ms: key=${key}${suffix}`;
133
+
134
+ if (outcome === 'failed') {
135
+ console.error(message);
136
+ } else if (outcome === 'miss') {
137
+ console.warn(message);
138
+ } else {
139
+ console.log(message);
140
+ }
141
+ },
142
+ };
143
+ }
144
+
145
+ function runObsRequest(operation, file, key, request) {
146
+ return new Promise((resolve, reject) => {
147
+ const requestLogger = createObsRequestLogger(operation, file, key);
148
+ const { operationTimeoutMs } = getObsRuntimeOptions();
149
+ let settled = false;
150
+ let operationTimer;
151
+ let activeRequest;
152
+
153
+ const finish = (error, result, outcome) => {
154
+ if (settled) {
155
+ return;
156
+ }
157
+
158
+ settled = true;
159
+ clearTimeout(operationTimer);
160
+ activeRequest = null;
161
+ requestLogger.finish(
162
+ outcome || (error ? 'failed' : 'completed'),
163
+ formatObsError(error || result),
164
+ );
165
+
166
+ if (error) {
167
+ reject(error);
168
+ } else {
169
+ resolve(result);
170
+ }
171
+ };
172
+
173
+ const armOperationTimeout = () => {
174
+ if (settled) {
175
+ return;
176
+ }
177
+ clearTimeout(operationTimer);
178
+ operationTimer = setTimeout(() => {
179
+ const requestToAbort = activeRequest;
180
+ const error = new Error(
181
+ `OBS ${operation} timed out after ${operationTimeoutMs / 1000}s without progress`,
182
+ );
183
+ error.code = 'OBS_OPERATION_TIMEOUT';
184
+ finish(error);
185
+ if (requestToAbort && requestToAbort.destroy) {
186
+ requestToAbort.destroy();
187
+ }
188
+ }, operationTimeoutMs);
189
+ };
190
+
191
+ const registerRequest = (clientRequest) => {
192
+ activeRequest = clientRequest;
193
+ };
194
+
195
+ armOperationTimeout();
196
+
197
+ try {
198
+ request(finish, armOperationTimeout, registerRequest);
199
+ } catch (error) {
200
+ finish(error);
201
+ }
202
+ });
203
+ }
204
+
205
+ function waitForObsClientReady(client) {
206
+ if (client.util && client.util.signatureContext && client.util.server) {
207
+ return Promise.resolve(client);
208
+ }
209
+
210
+ const { operationTimeoutMs } = getObsRuntimeOptions();
211
+ const startedAt = Date.now();
212
+
213
+ return new Promise((resolve, reject) => {
214
+ const checkReady = () => {
215
+ if (client.util && client.util.signatureContext && client.util.server) {
216
+ resolve(client);
217
+ return;
218
+ }
219
+
220
+ if (Date.now() - startedAt >= operationTimeoutMs) {
221
+ const error = new Error(
222
+ `OBS client initialization timed out after ${operationTimeoutMs / 1000}s`,
223
+ );
224
+ error.code = 'OBS_CLIENT_INIT_TIMEOUT';
225
+ reject(error);
226
+ return;
227
+ }
228
+
229
+ setImmediate(checkReady);
230
+ };
231
+
232
+ setImmediate(checkReady);
233
+ });
234
+ }
23
235
 
24
236
  function getObsClient() {
25
237
  if (!obsClient) {
238
+ const { timeoutSeconds, maxRetryCount } = getObsRuntimeOptions();
26
239
  obsClient = new OBSClient({
27
240
  ...obsConfig,
241
+ timeout: timeoutSeconds,
242
+ max_retry_count: maxRetryCount,
28
243
  });
244
+ // esdk-obs-nodejs 3.26.8 将 initFactory 改成了 async,但构造函数没有等待它。
245
+ // 立即请求会在 signatureContext 尚未赋值时破坏 SDK 的 bucket 协商锁。
246
+ obsClientReady = waitForObsClientReady(obsClient);
29
247
  }
30
- return obsClient;
248
+ return obsClientReady;
31
249
  }
32
250
 
33
251
  function getOssClient() {
@@ -41,49 +259,62 @@ function getOssClient() {
41
259
 
42
260
  function doOssUpload(file) {
43
261
  return getOssClient().put(
44
- compatiblePath(path.join(ossConfig.dir, path.relative(DIST_DIR, file))),
262
+ getRemoteKey(ossConfig, file),
45
263
  file,
46
264
  );
47
265
  }
48
266
 
49
267
  function getOssStat(file) {
50
- return getOssClient().head(compatiblePath(path.join(ossConfig.dir, path.relative(DIST_DIR, file))));
268
+ return getOssClient().head(getRemoteKey(ossConfig, file));
51
269
  }
52
270
 
53
- function doObsUpload(file) {
54
- return new Promise((resolve, reject) => {
55
- getObsClient().putObject(
271
+ async function doObsUpload(file) {
272
+ const key = getRemoteKey(obsConfig, file);
273
+ const client = await getObsClient();
274
+ return runObsRequest('upload', file, key, (finish, reportProgress, registerRequest) => {
275
+ client.putObject(
56
276
  {
57
277
  Bucket: obsConfig.bucket,
58
- Key: compatiblePath(path.join(obsConfig.dir, path.relative(DIST_DIR, file))),
278
+ Key: key,
59
279
  SourceFile: file,
280
+ ProgressCallback: reportProgress,
281
+ RequestHook: registerRequest,
60
282
  },
61
283
  (err, result) => {
284
+ const status = getObsStatus(result);
62
285
  if (err) {
63
- reject(err);
64
- } else if (result.CommonMsg.Status >= 300) {
65
- reject(new Error(`OBS upload failed with status ${result.CommonMsg.Status}`));
286
+ finish(err);
287
+ } else if (status === undefined) {
288
+ finish(getInvalidObsResponseError('upload'));
289
+ } else if (status >= 300) {
290
+ finish(result);
66
291
  } else {
67
- resolve(result);
292
+ finish(null, result);
68
293
  }
69
294
  },
70
295
  );
71
296
  });
72
297
  }
73
298
  async function getObsStat(file) {
74
- return new Promise((resolve, reject) => {
75
- getObsClient().getObjectMetadata(
299
+ const key = getRemoteKey(obsConfig, file);
300
+ const client = await getObsClient();
301
+ return runObsRequest('metadata check', file, key, (finish, _reportProgress, registerRequest) => {
302
+ client.getObjectMetadata(
76
303
  {
77
304
  Bucket: obsConfig.bucket,
78
- Key: compatiblePath(path.join(obsConfig.dir, path.relative(DIST_DIR, file))),
305
+ Key: key,
306
+ RequestHook: registerRequest,
79
307
  },
80
308
  (err, result) => {
309
+ const status = getObsStatus(result);
81
310
  if (err) {
82
- reject(err);
83
- } else if (result.CommonMsg.Status < 300) {
84
- resolve(result);
311
+ finish(err);
312
+ } else if (status === undefined) {
313
+ finish(getInvalidObsResponseError('metadata check'));
314
+ } else if (status < 300) {
315
+ finish(null, result);
85
316
  } else {
86
- reject(result);
317
+ finish(result, null, 'miss');
87
318
  }
88
319
  },
89
320
  );
@@ -111,8 +342,7 @@ function doUpload(file) {
111
342
  }
112
343
 
113
344
  function updateProgress() {
114
- const completed =
115
- Object.entries(uploadQueue).filter((item) => item[1] === 'completed').length + 1;
345
+ const completed = Object.values(uploadQueue).filter((status) => status === 'completed').length;
116
346
  const total = Object.keys(uploadQueue).length;
117
347
 
118
348
  if (!progress) {
@@ -121,49 +351,72 @@ function updateProgress() {
121
351
  width: 40,
122
352
  clear: true,
123
353
  });
354
+ } else {
355
+ // watch 构建期间可能继续追加资源,进度条总数需要同步扩容。
356
+ progress.total = total;
124
357
  }
125
358
 
126
359
  progress.tick();
127
360
 
128
- if (completed === total) {
129
- progress.tick({
130
- current: total,
131
- });
132
- progress = null;
133
- uploadQueue = {};
134
- }
135
-
136
361
  return `[${completed}/${total}]`;
137
362
  }
138
363
 
139
- async function checkUpload() {
140
- const files = Object.entries(uploadQueue)
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
- );
364
+ async function uploadFile(file) {
365
+ uploadQueue[file] = 'uploading';
164
366
 
165
- checkUpload();
367
+ try {
368
+ await getStat(file);
369
+ setStorage(file, true);
370
+ } catch (error) {
371
+ try {
372
+ await doUpload(file);
373
+ setStorage(file, true);
374
+ } catch (uploadError) {
375
+ console.error(
376
+ `[assets] upload failed: file=${getDisplayFile(file)}, ${formatObsError(uploadError)}`,
377
+ );
378
+ }
379
+ } finally {
380
+ uploadQueue[file] = 'completed';
381
+ updateProgress();
382
+ }
383
+ }
384
+
385
+ function checkUpload() {
386
+ // done hook 可能在上一次上传尚未结束时再次触发。复用同一个 runner,避免每次构建
387
+ // 都额外启动 10 个请求,最终耗尽 OBS/HTTP 连接池。
388
+ if (uploadRunner) {
389
+ return uploadRunner;
166
390
  }
391
+
392
+ uploadRunner = (async () => {
393
+ while (true) {
394
+ const files = Object.entries(uploadQueue)
395
+ .filter((item) => item[1] === 'pending')
396
+ .slice(0, UPLOAD_CONCURRENCY)
397
+ .map((item) => item[0]);
398
+
399
+ if (!files.length) {
400
+ break;
401
+ }
402
+
403
+ await Promise.all(files.map(uploadFile));
404
+ }
405
+ })().finally(() => {
406
+ uploadRunner = null;
407
+
408
+ // 文件状态已经写完后再清空,避免旧实现最后一个文件把已清空的队列重新写回。
409
+ if (!Object.values(uploadQueue).includes('pending')) {
410
+ progress = null;
411
+ uploadQueue = {};
412
+ return;
413
+ }
414
+
415
+ // 兜住 runner 完成与 watch 新增资源恰好交错的情况。
416
+ checkUpload();
417
+ });
418
+
419
+ return uploadRunner;
167
420
  }
168
421
 
169
422
  function addToUploadQueue(assets) {
@@ -172,13 +425,26 @@ function addToUploadQueue(assets) {
172
425
  return;
173
426
  }
174
427
 
428
+ let cachedCount = 0;
429
+ let pendingCount = 0;
430
+
175
431
  assets.forEach((asset) => {
176
432
  const file = path.resolve(DIST_DIR, asset);
433
+ if (getStorage(file)) {
434
+ cachedCount += 1;
435
+ return;
436
+ }
177
437
  if (uploadQueue[file] === undefined) {
178
- uploadQueue[file] = false;
438
+ uploadQueue[file] = 'pending';
439
+ pendingCount += 1;
179
440
  }
180
441
  });
181
- checkUpload();
442
+
443
+ console.log(
444
+ `[assets] upload queue: provider=${ossConfig ? 'OSS' : 'OBS'}, total=${assets.length}, ` +
445
+ `cached=${cachedCount}, pending=${pendingCount}`,
446
+ );
447
+ return pendingCount ? checkUpload() : Promise.resolve();
182
448
  }
183
449
 
184
450
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hw-weapp-compiler",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
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 && node ./test/upload-async-client.js"
56
57
  }
57
58
  }
@@ -0,0 +1,72 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const Module = require('module');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ class AsyncOBSClient {
8
+ constructor(config) {
9
+ this.config = config;
10
+ this.util = {
11
+ server: null,
12
+ signatureContext: null,
13
+ };
14
+
15
+ // 模拟 esdk-obs-nodejs 3.26.8:initFactory 在第一次 await 后才完成赋值。
16
+ Promise.resolve().then(() => {
17
+ this.util.server = config.server;
18
+ this.util.signatureContext = { signature: 'obs' };
19
+ });
20
+ }
21
+
22
+ getObjectMetadata(_params, callback) {
23
+ assert(this.util.signatureContext, 'request started before OBS client initialization');
24
+ callback(null, { CommonMsg: { Status: 200 } });
25
+ }
26
+ }
27
+
28
+ async function run() {
29
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hw-upload-async-client-test-'));
30
+ const originalCwd = process.cwd();
31
+ const originalLoad = Module._load;
32
+
33
+ try {
34
+ fs.mkdirSync(path.join(projectDir, 'dist/assets'), { recursive: true });
35
+ fs.writeFileSync(path.join(projectDir, 'dist/assets/ready.png'), 'ready');
36
+ fs.writeFileSync(
37
+ path.join(projectDir, '.weapp.js'),
38
+ `module.exports = { obsConfig: ${JSON.stringify({
39
+ access_key_id: 'test-ak',
40
+ secret_access_key: 'test-sk',
41
+ server: 'http://obs.example.com',
42
+ bucket: 'test-bucket',
43
+ dir: 'test-dir',
44
+ operation_timeout: 1,
45
+ })} };`,
46
+ );
47
+
48
+ Module._load = function load(request, parent, isMain) {
49
+ if (request === 'esdk-obs-nodejs') {
50
+ return AsyncOBSClient;
51
+ }
52
+ return originalLoad.call(this, request, parent, isMain);
53
+ };
54
+
55
+ process.chdir(projectDir);
56
+ const uploadModule = path.resolve(originalCwd, 'build/utils/upload.js');
57
+ const { addToUploadQueue } = require(uploadModule);
58
+ await addToUploadQueue(['assets/ready.png']);
59
+
60
+ // 等待 storage.js 的防抖写入结束后再清理测试目录。
61
+ await new Promise((resolve) => setTimeout(resolve, 400));
62
+ } finally {
63
+ Module._load = originalLoad;
64
+ process.chdir(originalCwd);
65
+ fs.rmSync(projectDir, { recursive: true, force: true });
66
+ }
67
+ }
68
+
69
+ run().catch((error) => {
70
+ console.error(error);
71
+ process.exitCode = 1;
72
+ });
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
+ });