@wwkit/cft 1.0.16 → 1.0.18
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/bin/index.js +23 -1
- package/package.json +2 -2
- package/scripts/postinstall.js +7 -2
- package/src/installer.js +71 -15
package/bin/index.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync } from 'node:child_process'
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
4
5
|
import { install, status, clear, showConfig, showEnv } from '../src/index.js'
|
|
5
6
|
|
|
7
|
+
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
8
|
+
|
|
6
9
|
const command = process.argv[2]
|
|
7
10
|
|
|
11
|
+
function parseInstallArgs(args) {
|
|
12
|
+
let proxy = ''
|
|
13
|
+
for (let i = 3; i < args.length; i++) {
|
|
14
|
+
const arg = args[i]
|
|
15
|
+
if (arg === '-p' || arg === '--proxy') {
|
|
16
|
+
proxy = args[i + 1] || ''
|
|
17
|
+
i++
|
|
18
|
+
} else if (arg.startsWith('--proxy=')) {
|
|
19
|
+
proxy = arg.slice('--proxy='.length)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return proxy
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
if (command === 'install') {
|
|
9
|
-
|
|
26
|
+
const proxy = parseInstallArgs(process.argv)
|
|
27
|
+
install(proxy ? { proxy } : {})
|
|
10
28
|
.then((info) => {
|
|
11
29
|
console.log()
|
|
12
30
|
console.log(' ✓ CFT 安装完成')
|
|
@@ -28,16 +46,20 @@ if (command === 'install') {
|
|
|
28
46
|
showEnv()
|
|
29
47
|
} else if (command === 'upgrade') {
|
|
30
48
|
execSync('npm install -g @wwkit/cft@latest', { stdio: 'inherit' })
|
|
49
|
+
} else if (command === 'version') {
|
|
50
|
+
console.log(pkgVersion)
|
|
31
51
|
} else if (command === 'help' || command === undefined) {
|
|
32
52
|
console.log(`cft - Chrome for Testing 安装器
|
|
33
53
|
|
|
34
54
|
用法:
|
|
35
55
|
cft install 下载并安装 Chrome for Testing(已装同版本则跳过)
|
|
56
|
+
cft install -p <proxy> 指定代理下载安装(socks5://host:port|http://host:port),覆盖配置
|
|
36
57
|
cft status 显示安装状态
|
|
37
58
|
cft config 显示用户配置(~/.config/cft/config.json5)
|
|
38
59
|
cft env 显示 cft 创建的全局环境变量
|
|
39
60
|
cft clear 删除安装产物(~/.config/cft/)
|
|
40
61
|
cft upgrade 升级 cft 自身(npm install -g @wwkit/cft@latest)
|
|
62
|
+
cft version 显示版本号
|
|
41
63
|
cft help 显示本帮助
|
|
42
64
|
|
|
43
65
|
安装目录: ~/.config/cft/`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwkit/cft",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"author": "bluesliu <langcai163@163.com>",
|
|
5
5
|
"description": "Chrome for Testing (CFT) installer — download, extract, chmod, dep-check",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"extract-zip": "^2.0.1",
|
|
39
|
-
"@wwkit/shared": "1.0.
|
|
39
|
+
"@wwkit/shared": "1.0.18"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jest": "^29.7.0"
|
package/scripts/postinstall.js
CHANGED
|
@@ -4,12 +4,17 @@ import { spawnSync } from 'node:child_process'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { Shell } from '@wwkit/shared'
|
|
7
|
-
import { getLocation, getOs } from '../src/index.js'
|
|
7
|
+
import { getLocation, getOs, getProxy } from '../src/index.js'
|
|
8
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
const bin = path.join(__dirname, '..', 'bin', 'index.js')
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// 回退链:npm 安装参数 --proxy(npm_config_proxy)> cft 配置 proxy
|
|
13
|
+
const proxy = process.env.npm_config_proxy || getProxy()
|
|
14
|
+
const args = [bin, 'install']
|
|
15
|
+
if (proxy) args.push('-p', proxy)
|
|
16
|
+
|
|
17
|
+
const result = spawnSync('node', args, { stdio: 'inherit' })
|
|
13
18
|
if (result.status !== 0) {
|
|
14
19
|
console.error('cft: postinstall (cft install) failed')
|
|
15
20
|
process.exit(1)
|
package/src/installer.js
CHANGED
|
@@ -12,39 +12,43 @@ import { getVersion, getRegistry, getProxy, getLocation, copyBuiltinConfig } fro
|
|
|
12
12
|
import { parseProxy, openTunnel } from './proxy.js'
|
|
13
13
|
|
|
14
14
|
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 cft-installer'
|
|
15
|
+
const MANUAL_DOWNLOAD_HINT = '资源下载失败,请在 http://deepbluenet.com/webwork/#/mirror 手动下载资源,并将资源解压到 ~/.config/cft 目录,然后将地址添加到 ~/.config/webwork/webwork.json5 中的 env.SELENIUM_CFT_DIR 属性上'
|
|
15
16
|
|
|
16
17
|
function log(msg) {
|
|
17
18
|
console.log(` ${msg}`)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function sendRequest(url, cb) {
|
|
21
|
-
const proxy = parseProxy(
|
|
21
|
+
function sendRequest(url, proxyValue, cb, method = 'GET') {
|
|
22
|
+
const proxy = parseProxy(proxyValue)
|
|
22
23
|
const target = new URL(url)
|
|
23
24
|
const headers = { 'user-agent': USER_AGENT }
|
|
24
25
|
const onError = (e) => cb(e, null)
|
|
25
26
|
|
|
26
27
|
if (!proxy) {
|
|
27
28
|
const proto = target.protocol === 'https:' ? https : http
|
|
28
|
-
const req = proto.
|
|
29
|
+
const req = proto.request(url, { headers, method }, (res) => cb(null, res))
|
|
29
30
|
req.on('error', onError)
|
|
31
|
+
req.end()
|
|
30
32
|
return
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
if (proxy.protocol === 'http:' && target.protocol === 'http:') {
|
|
34
|
-
const req = http.
|
|
36
|
+
const req = http.request({
|
|
35
37
|
host: proxy.hostname,
|
|
36
38
|
port: Number(proxy.port) || 80,
|
|
37
39
|
path: url,
|
|
40
|
+
method,
|
|
38
41
|
headers: { ...headers, host: target.host },
|
|
39
42
|
}, (res) => cb(null, res))
|
|
40
43
|
req.on('error', onError)
|
|
44
|
+
req.end()
|
|
41
45
|
return
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
openTunnel(proxy, target).then((socket) => {
|
|
45
49
|
const proto = target.protocol === 'https:' ? https : http
|
|
46
50
|
const req = proto.request({
|
|
47
|
-
method
|
|
51
|
+
method,
|
|
48
52
|
hostname: target.hostname,
|
|
49
53
|
port: Number(target.port) || (target.protocol === 'https:' ? 443 : 80),
|
|
50
54
|
path: target.pathname + target.search,
|
|
@@ -58,7 +62,37 @@ function sendRequest(url, cb) {
|
|
|
58
62
|
}).catch(onError)
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
/**
|
|
66
|
+
* 访问前预检完整路径是否可达(HEAD 请求,非 2xx/3xx 视为不可达)
|
|
67
|
+
* @param {string} url - 完整下载 URL
|
|
68
|
+
* @param {string} [proxyValue] - 代理地址
|
|
69
|
+
* @returns {Promise<boolean>}
|
|
70
|
+
*/
|
|
71
|
+
function checkUrlReachable(url, proxyValue) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
sendRequest(url, proxyValue, (err, response) => {
|
|
74
|
+
if (err) { resolve(false); return }
|
|
75
|
+
response.resume()
|
|
76
|
+
if (response.statusCode >= 200 && response.statusCode < 400) {
|
|
77
|
+
resolve(true)
|
|
78
|
+
} else {
|
|
79
|
+
resolve(false)
|
|
80
|
+
}
|
|
81
|
+
}, 'HEAD')
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 下载失败时的统一终止提示
|
|
87
|
+
* @param {Error} [cause] - 原始错误
|
|
88
|
+
*/
|
|
89
|
+
function downloadError(cause) {
|
|
90
|
+
const err = new Error(MANUAL_DOWNLOAD_HINT)
|
|
91
|
+
if (cause) err.cause = cause
|
|
92
|
+
return err
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function downloadFile(url, destPath, proxyValue, redirects = 0) {
|
|
62
96
|
return new Promise((resolve, reject) => {
|
|
63
97
|
if (redirects > 5) {
|
|
64
98
|
reject(new Error(`Too many redirects: ${url}`))
|
|
@@ -67,7 +101,7 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
67
101
|
const filename = path.basename(destPath)
|
|
68
102
|
log(`下载 ${filename} ...`)
|
|
69
103
|
|
|
70
|
-
sendRequest(url, (err, response) => {
|
|
104
|
+
sendRequest(url, proxyValue, (err, response) => {
|
|
71
105
|
if (err) { reject(err); return }
|
|
72
106
|
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
|
|
73
107
|
const location = response.headers.location
|
|
@@ -76,7 +110,7 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
76
110
|
reject(new Error(`Redirect without Location header: ${url}`))
|
|
77
111
|
return
|
|
78
112
|
}
|
|
79
|
-
downloadFile(location, destPath, redirects + 1).then(resolve).catch(reject)
|
|
113
|
+
downloadFile(location, destPath, proxyValue, redirects + 1).then(resolve).catch(reject)
|
|
80
114
|
return
|
|
81
115
|
}
|
|
82
116
|
if (response.statusCode !== 200) {
|
|
@@ -108,9 +142,9 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
108
142
|
})
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
async function downloadExtract(url, saveDir, filename, extractDir) {
|
|
145
|
+
async function downloadExtract(url, saveDir, filename, extractDir, proxyValue) {
|
|
112
146
|
const filePath = path.join(saveDir, filename)
|
|
113
|
-
await downloadFile(url, filePath)
|
|
147
|
+
await downloadFile(url, filePath, proxyValue)
|
|
114
148
|
|
|
115
149
|
log(`解压到 ${path.basename(extractDir)}`)
|
|
116
150
|
if (fs.existsSync(extractDir)) {
|
|
@@ -207,12 +241,19 @@ function checkSystemDeps(chromeExe, driverExe) {
|
|
|
207
241
|
}
|
|
208
242
|
}
|
|
209
243
|
|
|
210
|
-
|
|
244
|
+
/**
|
|
245
|
+
* 安装 CFT(Chrome + ChromeDriver)
|
|
246
|
+
* @param {object} [opts]
|
|
247
|
+
* @param {string} [opts.proxy] - 显式代理地址(socks5://host:port 等);传入则直接使用,不读配置
|
|
248
|
+
* @returns {Promise<object>}
|
|
249
|
+
*/
|
|
250
|
+
export async function install(opts = {}) {
|
|
211
251
|
copyBuiltinConfig()
|
|
212
252
|
const version = getVersion()
|
|
213
253
|
const registry = getRegistry()
|
|
214
254
|
const location = getLocation()
|
|
215
255
|
const osType = getOs()
|
|
256
|
+
const proxy = opts.proxy ?? getProxy()
|
|
216
257
|
|
|
217
258
|
if (!VALID_OS_TYPES.includes(osType)) {
|
|
218
259
|
throw new Error(`Unsupported OS: ${osType}`)
|
|
@@ -228,13 +269,28 @@ export async function install() {
|
|
|
228
269
|
|
|
229
270
|
const chromeUrl = `${registry}/${version}/${osType}/chrome-${osType}.zip`
|
|
230
271
|
const driverUrl = `${registry}/${version}/${osType}/chromedriver-${osType}.zip`
|
|
231
|
-
log(`从网络下载 CFT ${version} (${osType})`)
|
|
232
|
-
|
|
233
272
|
const chromeDir = path.join(location, `chrome-${osType}`)
|
|
234
273
|
const driverDir = path.join(location, `chromedriver-${osType}`)
|
|
235
274
|
|
|
236
|
-
|
|
237
|
-
|
|
275
|
+
log(`从网络下载 CFT ${version} (${osType})`)
|
|
276
|
+
if (proxy) log(`使用代理: ${proxy}`)
|
|
277
|
+
|
|
278
|
+
for (const url of [chromeUrl, driverUrl]) {
|
|
279
|
+
const ok = await checkUrlReachable(url, proxy)
|
|
280
|
+
if (ok) {
|
|
281
|
+
log(`[OK] 资源可达: ${url}`)
|
|
282
|
+
} else {
|
|
283
|
+
log(`[FAIL] 资源不可达: ${url}`)
|
|
284
|
+
throw downloadError()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await downloadExtract(chromeUrl, location, `chrome-${osType}.zip`, chromeDir, proxy)
|
|
290
|
+
await downloadExtract(driverUrl, location, `chromedriver-${osType}.zip`, driverDir, proxy)
|
|
291
|
+
} catch (cause) {
|
|
292
|
+
throw downloadError(cause)
|
|
293
|
+
}
|
|
238
294
|
|
|
239
295
|
const chromeExe = findExe(chromeDir, 'chrome')
|
|
240
296
|
const driverExe = findExe(driverDir, 'chromedriver')
|