browserwing 0.0.1 → 1.0.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/README.md +10 -0
- package/install.js +314 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,16 @@ pnpm add -g browserwing
|
|
|
15
15
|
yarn global add browserwing
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
### macOS Users - Important
|
|
19
|
+
|
|
20
|
+
If you encounter an error when running BrowserWing on macOS (app gets killed immediately), run this command:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
xattr -d com.apple.quarantine $(which browserwing)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This removes the quarantine attribute that macOS applies to downloaded executables. [Learn more](https://github.com/browserwing/browserwing/blob/main/docs/MACOS_INSTALLATION_FIX.md)
|
|
27
|
+
|
|
18
28
|
## Quick Start
|
|
19
29
|
|
|
20
30
|
```bash
|
package/install.js
CHANGED
|
@@ -1,120 +1,366 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* BrowserWing npm postinstall script
|
|
5
|
+
* Downloads the appropriate binary for the current platform
|
|
6
|
+
*/
|
|
7
|
+
|
|
3
8
|
const https = require('https');
|
|
9
|
+
const http = require('http');
|
|
4
10
|
const fs = require('fs');
|
|
5
11
|
const path = require('path');
|
|
6
12
|
const { execSync } = require('child_process');
|
|
13
|
+
const os = require('os');
|
|
7
14
|
|
|
8
15
|
const REPO = 'browserwing/browserwing';
|
|
9
|
-
const
|
|
16
|
+
const VERSION = require('./package.json').version;
|
|
17
|
+
const TAG = `v${VERSION}`;
|
|
10
18
|
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
// ANSI color codes
|
|
20
|
+
const colors = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
cyan: '\x1b[36m'
|
|
16
26
|
};
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
arm: 'armv7'
|
|
22
|
-
};
|
|
28
|
+
function log(message, color = 'reset') {
|
|
29
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
30
|
+
}
|
|
23
31
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
function logInfo(message) {
|
|
33
|
+
log(`[INFO] ${message}`, 'green');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function logWarning(message) {
|
|
37
|
+
log(`[WARNING] ${message}`, 'yellow');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function logError(message) {
|
|
41
|
+
log(`[ERROR] ${message}`, 'red');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logDebug(message) {
|
|
45
|
+
log(`[DEBUG] ${message}`, 'cyan');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Detect platform and architecture
|
|
49
|
+
function detectPlatform() {
|
|
50
|
+
const platform = os.platform();
|
|
51
|
+
const arch = os.arch();
|
|
52
|
+
|
|
53
|
+
let platformName;
|
|
54
|
+
let archName;
|
|
55
|
+
let isWindows = false;
|
|
56
|
+
|
|
57
|
+
// Map platform
|
|
58
|
+
switch (platform) {
|
|
59
|
+
case 'darwin':
|
|
60
|
+
platformName = 'darwin';
|
|
61
|
+
break;
|
|
62
|
+
case 'linux':
|
|
63
|
+
platformName = 'linux';
|
|
64
|
+
break;
|
|
65
|
+
case 'win32':
|
|
66
|
+
platformName = 'windows';
|
|
67
|
+
isWindows = true;
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Map architecture
|
|
74
|
+
switch (arch) {
|
|
75
|
+
case 'x64':
|
|
76
|
+
archName = 'amd64';
|
|
77
|
+
break;
|
|
78
|
+
case 'arm64':
|
|
79
|
+
archName = 'arm64';
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { platform: platformName, arch: archName, isWindows };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Test download speed and select fastest mirror
|
|
89
|
+
async function selectMirror(archiveName) {
|
|
90
|
+
logInfo('Testing download mirrors...');
|
|
91
|
+
|
|
92
|
+
const githubUrl = `https://github.com/${REPO}/releases/download/${TAG}/${archiveName}`;
|
|
93
|
+
const giteeUrl = `https://gitee.com/browserwing/browserwing/releases/download/${TAG}/${archiveName}`;
|
|
94
|
+
|
|
95
|
+
// Test GitHub
|
|
96
|
+
const githubTime = await testUrl(githubUrl);
|
|
97
|
+
logDebug(`GitHub response time: ${githubTime}ms`);
|
|
98
|
+
|
|
99
|
+
// Test Gitee
|
|
100
|
+
const giteeTime = await testUrl(giteeUrl);
|
|
101
|
+
logDebug(`Gitee response time: ${giteeTime}ms`);
|
|
102
|
+
|
|
103
|
+
// Select fastest
|
|
104
|
+
if (githubTime < giteeTime) {
|
|
105
|
+
logInfo('Using GitHub mirror (faster)');
|
|
106
|
+
return {
|
|
107
|
+
name: 'github',
|
|
108
|
+
url: `https://github.com/${REPO}/releases/download`
|
|
109
|
+
};
|
|
110
|
+
} else {
|
|
111
|
+
logInfo('Using Gitee mirror (faster)');
|
|
112
|
+
return {
|
|
113
|
+
name: 'gitee',
|
|
114
|
+
url: 'https://gitee.com/browserwing/browserwing/releases/download'
|
|
115
|
+
};
|
|
30
116
|
}
|
|
31
|
-
|
|
32
|
-
return { platform, arch };
|
|
33
117
|
}
|
|
34
118
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
119
|
+
// Test URL response time
|
|
120
|
+
function testUrl(url) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
124
|
+
|
|
125
|
+
const request = protocol.get(url, { method: 'HEAD', timeout: 5000 }, (res) => {
|
|
126
|
+
const elapsed = Date.now() - startTime;
|
|
127
|
+
request.abort();
|
|
128
|
+
resolve(elapsed);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
request.on('error', () => {
|
|
132
|
+
resolve(999999); // Return large number on error
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
request.on('timeout', () => {
|
|
136
|
+
request.abort();
|
|
137
|
+
resolve(999999);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Download file with retry and mirror fallback
|
|
143
|
+
async function downloadFile(url, dest, maxRetries = 3) {
|
|
144
|
+
let mirror = await selectMirror(path.basename(dest));
|
|
145
|
+
let currentUrl = `${mirror.url}/${TAG}/${path.basename(dest)}`;
|
|
39
146
|
|
|
40
|
-
|
|
147
|
+
logInfo('Downloading BrowserWing...');
|
|
148
|
+
logInfo(`Download URL: ${currentUrl}`);
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
151
|
+
try {
|
|
152
|
+
await downloadFileOnce(currentUrl, dest);
|
|
153
|
+
logInfo('Download completed');
|
|
154
|
+
return;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (i < maxRetries - 1) {
|
|
157
|
+
logWarning(`Download failed, retrying (${i + 1}/${maxRetries})...`);
|
|
158
|
+
|
|
159
|
+
// Switch mirror
|
|
160
|
+
if (mirror.name === 'github') {
|
|
161
|
+
mirror.name = 'gitee';
|
|
162
|
+
mirror.url = 'https://gitee.com/browserwing/browserwing/releases/download';
|
|
163
|
+
logInfo('Switching to Gitee mirror...');
|
|
164
|
+
} else {
|
|
165
|
+
mirror.name = 'github';
|
|
166
|
+
mirror.url = `https://github.com/${REPO}/releases/download`;
|
|
167
|
+
logInfo('Switching to GitHub mirror...');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
currentUrl = `${mirror.url}/${TAG}/${path.basename(dest)}`;
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
172
|
+
} else {
|
|
173
|
+
throw new Error(`Failed to download after ${maxRetries} attempts: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
41
177
|
}
|
|
42
178
|
|
|
43
|
-
|
|
179
|
+
// Download file once
|
|
180
|
+
function downloadFileOnce(url, dest) {
|
|
44
181
|
return new Promise((resolve, reject) => {
|
|
45
|
-
|
|
46
|
-
|
|
182
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
47
183
|
const file = fs.createWriteStream(dest);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
headers: { 'User-Agent': 'browserwing-npm-installer' }
|
|
51
|
-
}, (response) => {
|
|
52
|
-
// Handle redirects
|
|
184
|
+
|
|
185
|
+
const request = protocol.get(url, (response) => {
|
|
53
186
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
}).on('error', reject);
|
|
187
|
+
// Handle redirect
|
|
188
|
+
file.close();
|
|
189
|
+
fs.unlinkSync(dest);
|
|
190
|
+
downloadFileOnce(response.headers.location, dest).then(resolve).catch(reject);
|
|
191
|
+
return;
|
|
61
192
|
}
|
|
62
|
-
|
|
193
|
+
|
|
63
194
|
if (response.statusCode !== 200) {
|
|
64
|
-
|
|
195
|
+
file.close();
|
|
196
|
+
fs.unlinkSync(dest);
|
|
197
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
65
198
|
return;
|
|
66
199
|
}
|
|
67
|
-
|
|
200
|
+
|
|
68
201
|
response.pipe(file);
|
|
69
|
-
|
|
202
|
+
|
|
70
203
|
file.on('finish', () => {
|
|
71
204
|
file.close();
|
|
72
205
|
resolve();
|
|
73
206
|
});
|
|
74
|
-
})
|
|
75
|
-
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
request.on('error', (err) => {
|
|
210
|
+
file.close();
|
|
211
|
+
fs.unlinkSync(dest);
|
|
212
|
+
reject(err);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
file.on('error', (err) => {
|
|
216
|
+
file.close();
|
|
217
|
+
fs.unlinkSync(dest);
|
|
76
218
|
reject(err);
|
|
77
219
|
});
|
|
78
220
|
});
|
|
79
221
|
}
|
|
80
222
|
|
|
223
|
+
// Extract archive
|
|
224
|
+
function extractArchive(archivePath, destDir, isWindows) {
|
|
225
|
+
logInfo('Extracting archive...');
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
if (isWindows) {
|
|
229
|
+
// Windows: use PowerShell to extract zip
|
|
230
|
+
const psCommand = `Expand-Archive -Path "${archivePath}" -DestinationPath "${destDir}" -Force`;
|
|
231
|
+
execSync(`powershell -command "${psCommand}"`, { stdio: 'ignore' });
|
|
232
|
+
} else {
|
|
233
|
+
// Unix: use tar
|
|
234
|
+
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'ignore' });
|
|
235
|
+
}
|
|
236
|
+
logInfo('Extraction completed');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw new Error(`Failed to extract archive: ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Fix macOS code signature issue
|
|
243
|
+
function fixMacOSCodeSignature(binaryPath) {
|
|
244
|
+
logInfo('Fixing macOS code signature...');
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// Method 1: Try to remove quarantine attribute
|
|
248
|
+
try {
|
|
249
|
+
execSync(`xattr -d com.apple.quarantine "${binaryPath}" 2>/dev/null`, { stdio: 'ignore' });
|
|
250
|
+
logInfo('✓ Removed quarantine attribute');
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// Quarantine attribute may not exist, continue
|
|
253
|
+
logDebug('No quarantine attribute to remove');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Method 2: Try ad-hoc code signing
|
|
257
|
+
try {
|
|
258
|
+
execSync(`codesign -s - "${binaryPath}" 2>/dev/null`, { stdio: 'ignore' });
|
|
259
|
+
logInfo('✓ Applied ad-hoc code signature');
|
|
260
|
+
} catch (e) {
|
|
261
|
+
// codesign may fail, but that's ok if quarantine removal worked
|
|
262
|
+
logDebug('Ad-hoc signing skipped (may require manual approval)');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
logInfo('macOS security fix applied');
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Don't fail installation if this doesn't work
|
|
268
|
+
logWarning('Could not automatically fix macOS security settings');
|
|
269
|
+
logWarning('If the app fails to start, run: xattr -d com.apple.quarantine ' + binaryPath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Main installation function
|
|
81
274
|
async function install() {
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log('╔════════════════════════════════════════╗');
|
|
277
|
+
console.log('║ BrowserWing npm Installation ║');
|
|
278
|
+
console.log('╚════════════════════════════════════════╝');
|
|
279
|
+
console.log('');
|
|
280
|
+
|
|
82
281
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
282
|
+
// Detect platform
|
|
283
|
+
const { platform, arch, isWindows } = detectPlatform();
|
|
284
|
+
logInfo(`Detected platform: ${platform}-${arch}`);
|
|
285
|
+
|
|
286
|
+
// Determine archive and binary names
|
|
287
|
+
const archiveExt = isWindows ? '.zip' : '.tar.gz';
|
|
288
|
+
const binaryExt = isWindows ? '.exe' : '';
|
|
289
|
+
const archiveName = `browserwing-${platform}-${arch}${archiveExt}`;
|
|
290
|
+
const binaryName = `browserwing-${platform}-${arch}${binaryExt}`;
|
|
291
|
+
|
|
292
|
+
// Download paths
|
|
87
293
|
const binDir = path.join(__dirname, 'bin');
|
|
294
|
+
const archivePath = path.join(binDir, archiveName);
|
|
295
|
+
const binaryPath = path.join(binDir, `browserwing${binaryExt}`);
|
|
296
|
+
|
|
297
|
+
// Create bin directory
|
|
88
298
|
if (!fs.existsSync(binDir)) {
|
|
89
299
|
fs.mkdirSync(binDir, { recursive: true });
|
|
90
300
|
}
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
301
|
+
|
|
302
|
+
// Download archive
|
|
303
|
+
await downloadFile(archiveName, archivePath);
|
|
304
|
+
|
|
305
|
+
// Extract archive
|
|
306
|
+
extractArchive(archivePath, binDir, isWindows);
|
|
307
|
+
|
|
308
|
+
// Find extracted binary
|
|
309
|
+
const extractedBinary = path.join(binDir, binaryName);
|
|
310
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
311
|
+
throw new Error(`Binary not found after extraction: ${binaryName}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Move binary to final location
|
|
315
|
+
if (fs.existsSync(binaryPath)) {
|
|
316
|
+
fs.unlinkSync(binaryPath);
|
|
317
|
+
}
|
|
318
|
+
fs.renameSync(extractedBinary, binaryPath);
|
|
319
|
+
|
|
320
|
+
// Set executable permissions (Unix only)
|
|
321
|
+
if (!isWindows) {
|
|
102
322
|
fs.chmodSync(binaryPath, 0o755);
|
|
103
323
|
}
|
|
104
|
-
|
|
105
|
-
|
|
324
|
+
|
|
325
|
+
// Fix macOS code signature issue
|
|
326
|
+
if (platform === 'darwin') {
|
|
327
|
+
fixMacOSCodeSignature(binaryPath);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Cleanup archive
|
|
331
|
+
fs.unlinkSync(archivePath);
|
|
332
|
+
|
|
333
|
+
console.log('');
|
|
334
|
+
logInfo('BrowserWing installed successfully!');
|
|
106
335
|
console.log('');
|
|
107
336
|
console.log('Quick start:');
|
|
108
|
-
console.log(' browserwing --port 8080');
|
|
109
|
-
console.log(' Open http://localhost:8080');
|
|
337
|
+
console.log(' 1. Run: browserwing --port 8080');
|
|
338
|
+
console.log(' 2. Open: http://localhost:8080');
|
|
339
|
+
console.log('');
|
|
110
340
|
|
|
341
|
+
// macOS specific notice
|
|
342
|
+
if (platform === 'darwin') {
|
|
343
|
+
console.log(colors.yellow + '⚠️ macOS Users:' + colors.reset);
|
|
344
|
+
console.log(' If the app fails to start, run this command:');
|
|
345
|
+
console.log(' xattr -d com.apple.quarantine $(which browserwing)');
|
|
346
|
+
console.log('');
|
|
347
|
+
console.log(' See: https://github.com/' + REPO + '/blob/main/docs/MACOS_INSTALLATION_FIX.md');
|
|
348
|
+
console.log('');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log('Documentation: https://github.com/' + REPO);
|
|
352
|
+
console.log('中文文档: https://gitee.com/browserwing/browserwing');
|
|
353
|
+
console.log('');
|
|
354
|
+
|
|
111
355
|
} catch (error) {
|
|
112
|
-
|
|
113
|
-
console.
|
|
114
|
-
console.
|
|
115
|
-
console.
|
|
356
|
+
logError(error.message);
|
|
357
|
+
console.log('');
|
|
358
|
+
console.log('Manual installation:');
|
|
359
|
+
console.log(` Visit: https://github.com/${REPO}/releases/tag/${TAG}`);
|
|
360
|
+
console.log('');
|
|
116
361
|
process.exit(1);
|
|
117
362
|
}
|
|
118
363
|
}
|
|
119
364
|
|
|
365
|
+
// Run installation
|
|
120
366
|
install();
|