@akiojin/gwt 6.17.0 → 6.18.0
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/package.json +1 -1
- package/scripts/postinstall.js +168 -37
- package/scripts/postinstall.test.js +57 -0
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
* for the current platform from GitHub Releases.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createWriteStream,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
chmodSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
} from 'fs';
|
|
8
15
|
import { dirname, join } from 'path';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
17
|
import { get } from 'https';
|
|
11
18
|
|
|
12
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -17,6 +24,15 @@ const BIN_DIR = join(__dirname, '..', 'bin');
|
|
|
17
24
|
const BIN_NAME = process.platform === 'win32' ? 'gwt.exe' : 'gwt';
|
|
18
25
|
const BIN_PATH = join(BIN_DIR, BIN_NAME);
|
|
19
26
|
|
|
27
|
+
const RETRY_CONFIG = {
|
|
28
|
+
maxAttempts: 5,
|
|
29
|
+
initialDelayMs: 500,
|
|
30
|
+
backoffFactor: 2,
|
|
31
|
+
maxDelayMs: 5000,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const MAX_REDIRECTS = 5;
|
|
35
|
+
|
|
20
36
|
function getPlatformArtifact() {
|
|
21
37
|
const platform = process.platform;
|
|
22
38
|
const arch = process.arch;
|
|
@@ -41,71 +57,162 @@ function getPlatformArtifact() {
|
|
|
41
57
|
return artifact;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
function getPackageVersion() {
|
|
61
|
+
const packagePath = join(__dirname, '..', 'package.json');
|
|
62
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
63
|
+
if (!pkg.version) {
|
|
64
|
+
throw new Error('package.json does not contain a version field');
|
|
65
|
+
}
|
|
66
|
+
return pkg.version;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildReleaseDownloadUrl(version, artifact) {
|
|
70
|
+
return `https://github.com/${REPO}/releases/download/v${version}/${artifact}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getRetryDelayMs(attempt, config = RETRY_CONFIG) {
|
|
74
|
+
const delay = config.initialDelayMs * Math.pow(config.backoffFactor, Math.max(0, attempt - 1));
|
|
75
|
+
return Math.min(delay, config.maxDelayMs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isRetryableStatus(statusCode) {
|
|
79
|
+
return statusCode === 403 || statusCode === 404 || statusCode >= 500;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isRetryableError(error) {
|
|
83
|
+
if (error && typeof error.statusCode === 'number') {
|
|
84
|
+
return isRetryableStatus(error.statusCode);
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatFailureGuidance(version, artifact) {
|
|
90
|
+
const versionedUrl = version && version !== 'unknown'
|
|
91
|
+
? buildReleaseDownloadUrl(version, artifact)
|
|
92
|
+
: null;
|
|
93
|
+
return {
|
|
94
|
+
versionedUrl,
|
|
95
|
+
releasePageUrl: `https://github.com/${REPO}/releases`,
|
|
96
|
+
buildCommand: 'cargo build --release',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getReleaseAssetUrl(version, artifact, httpGet = get) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
46
102
|
const options = {
|
|
47
103
|
hostname: 'api.github.com',
|
|
48
|
-
path: `/repos/${REPO}/releases/
|
|
104
|
+
path: `/repos/${REPO}/releases/tags/v${version}`,
|
|
49
105
|
headers: {
|
|
50
106
|
'User-Agent': 'gwt-postinstall',
|
|
51
107
|
'Accept': 'application/vnd.github.v3+json',
|
|
52
108
|
},
|
|
53
109
|
};
|
|
54
110
|
|
|
55
|
-
|
|
111
|
+
const req = httpGet(options, (res) => {
|
|
56
112
|
let data = '';
|
|
57
113
|
res.on('data', (chunk) => data += chunk);
|
|
58
114
|
res.on('end', () => {
|
|
115
|
+
if (res.statusCode !== 200) {
|
|
116
|
+
resolve(null);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
59
119
|
try {
|
|
60
120
|
const release = JSON.parse(data);
|
|
61
|
-
const asset = release.assets?.find(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Fallback to direct URL pattern
|
|
66
|
-
resolve(`https://github.com/${REPO}/releases/latest/download/${artifact}`);
|
|
67
|
-
}
|
|
68
|
-
} catch (e) {
|
|
69
|
-
// Fallback to direct URL pattern
|
|
70
|
-
resolve(`https://github.com/${REPO}/releases/latest/download/${artifact}`);
|
|
121
|
+
const asset = release.assets?.find((entry) => entry.name === artifact);
|
|
122
|
+
resolve(asset?.browser_download_url ?? null);
|
|
123
|
+
} catch {
|
|
124
|
+
resolve(null);
|
|
71
125
|
}
|
|
72
126
|
});
|
|
73
|
-
})
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
req.on('error', () => resolve(null));
|
|
74
130
|
});
|
|
75
131
|
}
|
|
76
132
|
|
|
77
|
-
async function
|
|
133
|
+
async function getDownloadUrl(version, artifact) {
|
|
134
|
+
const assetUrl = await getReleaseAssetUrl(version, artifact);
|
|
135
|
+
return assetUrl ?? buildReleaseDownloadUrl(version, artifact);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function downloadFile(url, dest, httpGet = get) {
|
|
78
139
|
return new Promise((resolve, reject) => {
|
|
79
|
-
const
|
|
140
|
+
const request = (currentUrl, redirectCount) => {
|
|
141
|
+
const req = httpGet(currentUrl, (res) => {
|
|
142
|
+
const statusCode = res.statusCode ?? 0;
|
|
80
143
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
if ([301, 302, 303, 307, 308].includes(statusCode)) {
|
|
145
|
+
const location = res.headers.location;
|
|
146
|
+
res.resume();
|
|
147
|
+
if (!location) {
|
|
148
|
+
const error = new Error(`Redirect without location (HTTP ${statusCode})`);
|
|
149
|
+
error.statusCode = statusCode;
|
|
150
|
+
reject(error);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (redirectCount >= MAX_REDIRECTS) {
|
|
154
|
+
const error = new Error('Too many redirects');
|
|
155
|
+
error.statusCode = statusCode;
|
|
156
|
+
reject(error);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const resolved = new URL(location, currentUrl).toString();
|
|
160
|
+
request(resolved, redirectCount + 1);
|
|
86
161
|
return;
|
|
87
162
|
}
|
|
88
163
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
164
|
+
if (statusCode !== 200) {
|
|
165
|
+
res.resume();
|
|
166
|
+
const error = new Error(`Failed to download: HTTP ${statusCode}`);
|
|
167
|
+
error.statusCode = statusCode;
|
|
168
|
+
reject(error);
|
|
91
169
|
return;
|
|
92
170
|
}
|
|
93
171
|
|
|
172
|
+
const file = createWriteStream(dest);
|
|
94
173
|
res.pipe(file);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
174
|
+
|
|
175
|
+
const cleanup = (err) => {
|
|
176
|
+
file.close(() => {
|
|
177
|
+
if (existsSync(dest)) unlinkSync(dest);
|
|
178
|
+
reject(err);
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
file.on('finish', () => file.close(resolve));
|
|
183
|
+
file.on('error', cleanup);
|
|
184
|
+
res.on('error', cleanup);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
req.on('error', (err) => {
|
|
100
188
|
if (existsSync(dest)) unlinkSync(dest);
|
|
101
189
|
reject(err);
|
|
102
190
|
});
|
|
103
191
|
};
|
|
104
192
|
|
|
105
|
-
request(url);
|
|
193
|
+
request(url, 0);
|
|
106
194
|
});
|
|
107
195
|
}
|
|
108
196
|
|
|
197
|
+
async function downloadWithRetry(url, dest, config = RETRY_CONFIG) {
|
|
198
|
+
let attempt = 0;
|
|
199
|
+
|
|
200
|
+
while (attempt < config.maxAttempts) {
|
|
201
|
+
attempt += 1;
|
|
202
|
+
try {
|
|
203
|
+
await downloadFile(url, dest);
|
|
204
|
+
return;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!isRetryableError(error) || attempt >= config.maxAttempts) {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
const delayMs = getRetryDelayMs(attempt, config);
|
|
210
|
+
console.log(`Retrying download in ${delayMs}ms (attempt ${attempt}/${config.maxAttempts})...`);
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
109
216
|
async function main() {
|
|
110
217
|
// Skip in CI environments where binary might be built differently
|
|
111
218
|
if (process.env.CI || process.env.GITHUB_ACTIONS) {
|
|
@@ -120,17 +227,19 @@ async function main() {
|
|
|
120
227
|
}
|
|
121
228
|
|
|
122
229
|
const artifact = getPlatformArtifact();
|
|
230
|
+
let version = 'unknown';
|
|
123
231
|
console.log(`Downloading gwt binary for ${process.platform}-${process.arch}...`);
|
|
124
232
|
|
|
125
233
|
try {
|
|
126
|
-
|
|
234
|
+
version = getPackageVersion();
|
|
235
|
+
const url = await getDownloadUrl(version, artifact);
|
|
127
236
|
console.log(`Downloading from: ${url}`);
|
|
128
237
|
|
|
129
238
|
if (!existsSync(BIN_DIR)) {
|
|
130
239
|
mkdirSync(BIN_DIR, { recursive: true });
|
|
131
240
|
}
|
|
132
241
|
|
|
133
|
-
await
|
|
242
|
+
await downloadWithRetry(url, BIN_PATH);
|
|
134
243
|
|
|
135
244
|
// Make executable on Unix
|
|
136
245
|
if (process.platform !== 'win32') {
|
|
@@ -139,14 +248,36 @@ async function main() {
|
|
|
139
248
|
|
|
140
249
|
console.log('gwt binary installed successfully!');
|
|
141
250
|
} catch (error) {
|
|
251
|
+
const guidance = formatFailureGuidance(version, artifact);
|
|
142
252
|
console.error('Failed to download gwt binary:', error.message);
|
|
143
253
|
console.error('');
|
|
144
254
|
console.error('You can manually download the binary from:');
|
|
145
|
-
|
|
255
|
+
if (guidance.versionedUrl) {
|
|
256
|
+
console.error(guidance.versionedUrl);
|
|
257
|
+
}
|
|
258
|
+
console.error(guidance.releasePageUrl);
|
|
146
259
|
console.error('');
|
|
147
|
-
console.error(
|
|
260
|
+
console.error(`Or build from source with: ${guidance.buildCommand}`);
|
|
148
261
|
process.exit(1);
|
|
149
262
|
}
|
|
150
263
|
}
|
|
151
264
|
|
|
152
|
-
|
|
265
|
+
const isDirectRun = process.argv[1]
|
|
266
|
+
&& pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
267
|
+
|
|
268
|
+
if (isDirectRun) {
|
|
269
|
+
main();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export {
|
|
273
|
+
RETRY_CONFIG,
|
|
274
|
+
buildReleaseDownloadUrl,
|
|
275
|
+
formatFailureGuidance,
|
|
276
|
+
getRetryDelayMs,
|
|
277
|
+
getReleaseAssetUrl,
|
|
278
|
+
getDownloadUrl,
|
|
279
|
+
getPackageVersion,
|
|
280
|
+
getPlatformArtifact,
|
|
281
|
+
isRetryableError,
|
|
282
|
+
isRetryableStatus,
|
|
283
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
RETRY_CONFIG,
|
|
6
|
+
buildReleaseDownloadUrl,
|
|
7
|
+
formatFailureGuidance,
|
|
8
|
+
getRetryDelayMs,
|
|
9
|
+
isRetryableError,
|
|
10
|
+
isRetryableStatus,
|
|
11
|
+
} from './postinstall.js';
|
|
12
|
+
|
|
13
|
+
test('buildReleaseDownloadUrl uses version tag', () => {
|
|
14
|
+
const url = buildReleaseDownloadUrl('6.17.0', 'gwt-linux-x86_64');
|
|
15
|
+
assert.equal(
|
|
16
|
+
url,
|
|
17
|
+
'https://github.com/akiojin/gwt/releases/download/v6.17.0/gwt-linux-x86_64',
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('getRetryDelayMs applies exponential backoff with cap', () => {
|
|
22
|
+
assert.equal(getRetryDelayMs(1, RETRY_CONFIG), 500);
|
|
23
|
+
assert.equal(getRetryDelayMs(2, RETRY_CONFIG), 1000);
|
|
24
|
+
assert.equal(getRetryDelayMs(3, RETRY_CONFIG), 2000);
|
|
25
|
+
assert.equal(getRetryDelayMs(5, RETRY_CONFIG), 5000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('isRetryableStatus matches 403/404/5xx', () => {
|
|
29
|
+
assert.equal(isRetryableStatus(403), true);
|
|
30
|
+
assert.equal(isRetryableStatus(404), true);
|
|
31
|
+
assert.equal(isRetryableStatus(500), true);
|
|
32
|
+
assert.equal(isRetryableStatus(429), false);
|
|
33
|
+
assert.equal(isRetryableStatus(400), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('isRetryableError uses statusCode or treats network errors as retryable', () => {
|
|
37
|
+
const notFound = new Error('not found');
|
|
38
|
+
notFound.statusCode = 404;
|
|
39
|
+
assert.equal(isRetryableError(notFound), true);
|
|
40
|
+
|
|
41
|
+
const badRequest = new Error('bad request');
|
|
42
|
+
badRequest.statusCode = 400;
|
|
43
|
+
assert.equal(isRetryableError(badRequest), false);
|
|
44
|
+
|
|
45
|
+
const networkError = new Error('socket hang up');
|
|
46
|
+
assert.equal(isRetryableError(networkError), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('formatFailureGuidance includes versioned URL and release page', () => {
|
|
50
|
+
const guidance = formatFailureGuidance('1.2.3', 'gwt-linux-x86_64');
|
|
51
|
+
assert.equal(
|
|
52
|
+
guidance.versionedUrl,
|
|
53
|
+
'https://github.com/akiojin/gwt/releases/download/v1.2.3/gwt-linux-x86_64',
|
|
54
|
+
);
|
|
55
|
+
assert.equal(guidance.releasePageUrl, 'https://github.com/akiojin/gwt/releases');
|
|
56
|
+
assert.equal(guidance.buildCommand, 'cargo build --release');
|
|
57
|
+
});
|