@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 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
- install()
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.16",
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.16"
39
+ "@wwkit/shared": "1.0.18"
40
40
  },
41
41
  "devDependencies": {
42
42
  "jest": "^29.7.0"
@@ -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
- const result = spawnSync('node', [bin, 'install'], { stdio: 'inherit' })
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(getProxy())
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.get(url, { headers }, (res) => cb(null, res))
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.get({
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: 'GET',
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
- function downloadFile(url, destPath, redirects = 0) {
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
- export async function install() {
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
- await downloadExtract(chromeUrl, location, `chrome-${osType}.zip`, chromeDir)
237
- await downloadExtract(driverUrl, location, `chromedriver-${osType}.zip`, driverDir)
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')