@wwkit/cft 1.0.17 → 1.0.19

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
@@ -2,14 +2,29 @@
2
2
 
3
3
  import { execSync } from 'node:child_process'
4
4
  import { readFileSync } from 'node:fs'
5
- import { install, status, clear, showConfig, showEnv } from '../src/index.js'
5
+ import { install, status, clear, showConfig, showEnv, getProxy } from '../src/index.js'
6
6
 
7
7
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
8
8
 
9
9
  const command = process.argv[2]
10
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
+
11
25
  if (command === 'install') {
12
- install()
26
+ const proxy = parseInstallArgs(process.argv)
27
+ install(proxy ? { proxy } : {})
13
28
  .then((info) => {
14
29
  console.log()
15
30
  console.log(' ✓ CFT 安装完成')
@@ -30,7 +45,10 @@ if (command === 'install') {
30
45
  } else if (command === 'env') {
31
46
  showEnv()
32
47
  } else if (command === 'upgrade') {
33
- execSync('npm install -g @wwkit/cft@latest', { stdio: 'inherit' })
48
+ const proxy = getProxy()
49
+ const npmArgs = ['install', '-g', '@wwkit/cft@latest']
50
+ if (proxy) npmArgs.push('--proxy', proxy)
51
+ execSync(`npm ${npmArgs.join(' ')}`, { stdio: 'inherit', shell: true })
34
52
  } else if (command === 'version') {
35
53
  console.log(pkgVersion)
36
54
  } else if (command === 'help' || command === undefined) {
@@ -38,6 +56,7 @@ if (command === 'install') {
38
56
 
39
57
  用法:
40
58
  cft install 下载并安装 Chrome for Testing(已装同版本则跳过)
59
+ cft install -p <proxy> 指定代理下载安装(socks5://host:port|http://host:port),覆盖配置
41
60
  cft status 显示安装状态
42
61
  cft config 显示用户配置(~/.config/cft/config.json5)
43
62
  cft env 显示 cft 创建的全局环境变量
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwkit/cft",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
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.17"
39
+ "@wwkit/shared": "1.0.19"
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/config.js CHANGED
@@ -9,6 +9,8 @@ const INSTALL_DIR = getXdgConfigDir('cft')
9
9
  const _loader = createConfigLoader({
10
10
  builtinConfigPath: path.join(__dirname, 'config.json5'),
11
11
  userConfigDir: INSTALL_DIR,
12
+ envContentVar: 'CFT_CONFIG_CONTENT',
13
+ envDirVar: 'CFT_CONFIG_DIR',
12
14
  })
13
15
 
14
16
  export const INSTALL_LOCATION = INSTALL_DIR
package/src/installer.js CHANGED
@@ -9,42 +9,46 @@ import { getOs, VALID_OS_TYPES } from './platform.js'
9
9
  import { find as findExe } from './exefinder.js'
10
10
  import { check as checkDeps, mapToPackages, install as installDeps, isSupported as depsSupported, detectPackageManager } from './depchecker.js'
11
11
  import { getVersion, getRegistry, getProxy, getLocation, copyBuiltinConfig } from './config.js'
12
- import { parseProxy, openTunnel } from './proxy.js'
12
+ import { parseProxy, openTunnel } from '@wwkit/shared'
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,40 @@ 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 if (response.statusCode === 405) {
79
+ // 405 Method Not Allowed — 服务器已响应,仅不支持 HEAD
80
+ resolve(true)
81
+ } else {
82
+ resolve(false)
83
+ }
84
+ }, 'HEAD')
85
+ })
86
+ }
87
+
88
+ /**
89
+ * 下载失败时的统一终止提示
90
+ * @param {Error} [cause] - 原始错误
91
+ */
92
+ function downloadError(cause) {
93
+ const err = new Error(MANUAL_DOWNLOAD_HINT)
94
+ if (cause) err.cause = cause
95
+ return err
96
+ }
97
+
98
+ function downloadFile(url, destPath, proxyValue, redirects = 0) {
62
99
  return new Promise((resolve, reject) => {
63
100
  if (redirects > 5) {
64
101
  reject(new Error(`Too many redirects: ${url}`))
@@ -67,7 +104,7 @@ function downloadFile(url, destPath, redirects = 0) {
67
104
  const filename = path.basename(destPath)
68
105
  log(`下载 ${filename} ...`)
69
106
 
70
- sendRequest(url, (err, response) => {
107
+ sendRequest(url, proxyValue, (err, response) => {
71
108
  if (err) { reject(err); return }
72
109
  if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
73
110
  const location = response.headers.location
@@ -76,7 +113,7 @@ function downloadFile(url, destPath, redirects = 0) {
76
113
  reject(new Error(`Redirect without Location header: ${url}`))
77
114
  return
78
115
  }
79
- downloadFile(location, destPath, redirects + 1).then(resolve).catch(reject)
116
+ downloadFile(location, destPath, proxyValue, redirects + 1).then(resolve).catch(reject)
80
117
  return
81
118
  }
82
119
  if (response.statusCode !== 200) {
@@ -108,9 +145,9 @@ function downloadFile(url, destPath, redirects = 0) {
108
145
  })
109
146
  }
110
147
 
111
- async function downloadExtract(url, saveDir, filename, extractDir) {
148
+ async function downloadExtract(url, saveDir, filename, extractDir, proxyValue) {
112
149
  const filePath = path.join(saveDir, filename)
113
- await downloadFile(url, filePath)
150
+ await downloadFile(url, filePath, proxyValue)
114
151
 
115
152
  log(`解压到 ${path.basename(extractDir)}`)
116
153
  if (fs.existsSync(extractDir)) {
@@ -207,12 +244,19 @@ function checkSystemDeps(chromeExe, driverExe) {
207
244
  }
208
245
  }
209
246
 
210
- export async function install() {
247
+ /**
248
+ * 安装 CFT(Chrome + ChromeDriver)
249
+ * @param {object} [opts]
250
+ * @param {string} [opts.proxy] - 显式代理地址(socks5://host:port 等);传入则直接使用,不读配置
251
+ * @returns {Promise<object>}
252
+ */
253
+ export async function install(opts = {}) {
211
254
  copyBuiltinConfig()
212
255
  const version = getVersion()
213
256
  const registry = getRegistry()
214
257
  const location = getLocation()
215
258
  const osType = getOs()
259
+ const proxy = opts.proxy || getProxy()
216
260
 
217
261
  if (!VALID_OS_TYPES.includes(osType)) {
218
262
  throw new Error(`Unsupported OS: ${osType}`)
@@ -228,13 +272,28 @@ export async function install() {
228
272
 
229
273
  const chromeUrl = `${registry}/${version}/${osType}/chrome-${osType}.zip`
230
274
  const driverUrl = `${registry}/${version}/${osType}/chromedriver-${osType}.zip`
231
- log(`从网络下载 CFT ${version} (${osType})`)
232
-
233
275
  const chromeDir = path.join(location, `chrome-${osType}`)
234
276
  const driverDir = path.join(location, `chromedriver-${osType}`)
235
277
 
236
- await downloadExtract(chromeUrl, location, `chrome-${osType}.zip`, chromeDir)
237
- await downloadExtract(driverUrl, location, `chromedriver-${osType}.zip`, driverDir)
278
+ log(`从网络下载 CFT ${version} (${osType})`)
279
+ if (proxy) log(`使用代理: ${proxy}`)
280
+
281
+ for (const url of [chromeUrl, driverUrl]) {
282
+ const ok = await checkUrlReachable(url, proxy)
283
+ if (ok) {
284
+ log(`[OK] 资源可达: ${url}`)
285
+ } else {
286
+ log(`[FAIL] 资源不可达: ${url}`)
287
+ throw downloadError()
288
+ }
289
+ }
290
+
291
+ try {
292
+ await downloadExtract(chromeUrl, location, `chrome-${osType}.zip`, chromeDir, proxy)
293
+ await downloadExtract(driverUrl, location, `chromedriver-${osType}.zip`, driverDir, proxy)
294
+ } catch (cause) {
295
+ throw downloadError(cause)
296
+ }
238
297
 
239
298
  const chromeExe = findExe(chromeDir, 'chrome')
240
299
  const driverExe = findExe(driverDir, 'chromedriver')
package/src/proxy.js DELETED
@@ -1,72 +0,0 @@
1
- import net from 'node:net'
2
- import http from 'node:http'
3
-
4
- export function parseProxy(raw) {
5
- if (!raw) return null
6
- try {
7
- const u = new URL(raw)
8
- if (!u.hostname) return null
9
- if (u.protocol !== 'http:' && u.protocol !== 'https:' && u.protocol !== 'socks5:' && u.protocol !== 'socks:') {
10
- return null
11
- }
12
- return u
13
- } catch {
14
- return null
15
- }
16
- }
17
-
18
- function socks5Connect(proxy, host, port) {
19
- return new Promise((resolve, reject) => {
20
- const sock = net.connect(Number(proxy.port) || 1080, proxy.hostname)
21
- let phase = 'greeting'
22
- const cleanup = (err) => { sock.removeAllListeners(); sock.destroy(); reject(err) }
23
- sock.once('error', cleanup)
24
- sock.once('connect', () => {
25
- sock.write(Buffer.from([0x05, 0x01, 0x00]))
26
- })
27
- sock.on('data', (data) => {
28
- if (phase === 'greeting') {
29
- if (data[0] !== 0x05 || data[1] !== 0x00) { cleanup(new Error(`socks5 代理拒绝无认证 (method=${data[1]})`)); return }
30
- phase = 'connect'
31
- const hostBuf = Buffer.from(host, 'utf8')
32
- const req = Buffer.alloc(7 + hostBuf.length)
33
- req[0] = 0x05; req[1] = 0x01; req[2] = 0x00; req[3] = 0x03
34
- req[4] = hostBuf.length
35
- hostBuf.copy(req, 5)
36
- req.writeUInt16BE(port, 5 + hostBuf.length)
37
- sock.write(req)
38
- } else if (phase === 'connect') {
39
- if (data[0] !== 0x05 || data[1] !== 0x00) { cleanup(new Error(`socks5 连接失败 (code=${data[1]})`)); return }
40
- sock.removeAllListeners('data')
41
- sock.removeListener('error', cleanup)
42
- resolve(sock)
43
- }
44
- })
45
- })
46
- }
47
-
48
- function httpConnect(proxy, host, port) {
49
- return new Promise((resolve, reject) => {
50
- const req = http.request({
51
- method: 'CONNECT',
52
- host: proxy.hostname,
53
- port: Number(proxy.port) || 80,
54
- path: `${host}:${port}`,
55
- })
56
- req.once('error', reject)
57
- req.once('connect', (res, socket) => {
58
- if (res.statusCode !== 200) { socket.destroy(); reject(new Error(`HTTP 代理 CONNECT 返回 ${res.statusCode}`)); return }
59
- resolve(socket)
60
- })
61
- req.end()
62
- })
63
- }
64
-
65
- export async function openTunnel(proxy, target) {
66
- const host = target.hostname
67
- const port = Number(target.port) || (target.protocol === 'https:' ? 443 : 80)
68
- if (proxy.protocol === 'socks5:' || proxy.protocol === 'socks:') {
69
- return socks5Connect(proxy, host, port)
70
- }
71
- return httpConnect(proxy, host, port)
72
- }