com.jimuwd.xian.registry-proxy 1.0.10 → 1.0.12
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/LICENSE +21 -0
- package/README.MD +148 -82
- package/dist/index.js +166 -133
- package/package.json +1 -1
- package/src/index.ts +200 -149
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 jimuwd.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.MD
CHANGED
|
@@ -73,8 +73,7 @@ unsafeHttpWhitelist:
|
|
|
73
73
|
```bash
|
|
74
74
|
#!/bin/bash
|
|
75
75
|
|
|
76
|
-
#
|
|
77
|
-
set -e # 命令失败时退出
|
|
76
|
+
# 启用严格模式,但移除 set -e,手动处理错误
|
|
78
77
|
set -u # 未定义变量时退出
|
|
79
78
|
set -o pipefail # 管道中任一命令失败时退出
|
|
80
79
|
|
|
@@ -98,46 +97,83 @@ PROJECT_ROOT=$(find_project_root)
|
|
|
98
97
|
LOCK_FILE="$PROJECT_ROOT/.registry-proxy-install.lock"
|
|
99
98
|
PORT_FILE="$PROJECT_ROOT/.registry-proxy-port"
|
|
100
99
|
|
|
101
|
-
#
|
|
100
|
+
# 检查是否已经在运行(通过锁文件)
|
|
101
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
102
|
+
echo "Custom install script is already running (lock file $LOCK_FILE exists)."
|
|
103
|
+
echo "If this is unexpected, please remove $LOCK_FILE and try again."
|
|
104
|
+
exit 0 # 内层脚本直接退出,表示任务已由外层脚本完成
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# 创建锁文件
|
|
108
|
+
touch "$LOCK_FILE"
|
|
109
|
+
|
|
110
|
+
# 清理函数,支持不同的退出状态
|
|
111
|
+
# 参数 $1:退出状态(0 表示正常退出,1 表示异常退出)
|
|
102
112
|
cleanup() {
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
local exit_code=${1:-1} # 默认退出码为 1(异常退出)
|
|
114
|
+
|
|
115
|
+
# 显式清除 EXIT 信号的 trap,避免潜在的误解
|
|
116
|
+
trap - EXIT
|
|
117
|
+
|
|
118
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
119
|
+
echo "Cleaning up after successful execution..."
|
|
120
|
+
else
|
|
121
|
+
echo "Caught interrupt signal or error, cleaning up..."
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# 清理临时文件
|
|
125
|
+
rm -f "$LOCK_FILE" 2>/dev/null
|
|
126
|
+
rm -f "$PORT_FILE" 2>/dev/null
|
|
127
|
+
|
|
128
|
+
# 停止代理服务器
|
|
105
129
|
if [ -n "${PROXY_PID:-}" ]; then
|
|
106
|
-
echo "Stopping proxy server..."
|
|
130
|
+
echo "Stopping proxy server (PID: $PROXY_PID)..."
|
|
107
131
|
kill -TERM "$PROXY_PID" 2>/dev/null
|
|
108
|
-
wait "$PROXY_PID" 2>/dev/null ||
|
|
109
|
-
rm -f "$PORT_FILE"
|
|
132
|
+
wait "$PROXY_PID" 2>/dev/null || true
|
|
110
133
|
echo "Proxy server stopped."
|
|
111
134
|
fi
|
|
112
|
-
|
|
135
|
+
|
|
136
|
+
# 切换到项目根目录
|
|
137
|
+
cd "$PROJECT_ROOT"
|
|
138
|
+
|
|
139
|
+
# 清理 npmRegistryServer 配置
|
|
140
|
+
yarn config unset npmRegistryServer 2>/dev/null || true
|
|
141
|
+
echo "Cleared npmRegistryServer configuration"
|
|
142
|
+
|
|
143
|
+
# 根据退出状态退出
|
|
144
|
+
exit "$exit_code"
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
# 注册信号处理
|
|
116
|
-
trap cleanup SIGINT SIGTERM
|
|
148
|
+
trap 'cleanup 1' SIGINT SIGTERM EXIT # 异常退出时调用 cleanup,退出码为 1
|
|
117
149
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
echo "Custom install script is already running, skipping to avoid loop."
|
|
121
|
-
exit 0
|
|
122
|
-
fi
|
|
123
|
-
|
|
124
|
-
# 创建锁文件
|
|
125
|
-
touch "$LOCK_FILE"
|
|
150
|
+
# 切换到项目根目录
|
|
151
|
+
cd "$PROJECT_ROOT"
|
|
126
152
|
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
# 使用 yarn dlx 直接运行 registry-proxy,放入后台运行
|
|
154
|
+
REGISTRY_PROXY_VERSION="${REGISTRY_PROXY_VERSION:-latest}"
|
|
155
|
+
echo "Starting registry-proxy@$REGISTRY_PROXY_VERSION in the background (logs will be displayed below)..."
|
|
156
|
+
yarn dlx com.jimuwd.xian.registry-proxy@"$REGISTRY_PROXY_VERSION" .registry-proxy.yml .yarnrc.yml ~/.yarnrc.yml &
|
|
131
157
|
PROXY_PID=$!
|
|
132
158
|
|
|
133
|
-
# 等待代理服务器启动并写入端口,最多
|
|
134
|
-
echo "Waiting for proxy server to start..."
|
|
135
|
-
for i in {1..
|
|
159
|
+
# 等待代理服务器启动并写入端口,最多 30 秒
|
|
160
|
+
echo "Waiting for proxy server to start (up to 30 seconds)..."
|
|
161
|
+
for i in {1..300}; do # 300 次循环,每次 0.1 秒,总共 30 秒
|
|
136
162
|
if [ -f "$PORT_FILE" ]; then
|
|
137
163
|
PROXY_PORT=$(cat "$PORT_FILE")
|
|
164
|
+
if [ -z "$PROXY_PORT" ]; then
|
|
165
|
+
echo "Error: Port file $PORT_FILE is empty"
|
|
166
|
+
cleanup 1
|
|
167
|
+
fi
|
|
138
168
|
if nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
139
169
|
echo "Proxy server is ready on port $PROXY_PORT!"
|
|
140
170
|
break
|
|
171
|
+
else
|
|
172
|
+
# 检查端口是否被占用
|
|
173
|
+
if netstat -tuln 2>/dev/null | grep -q ":$PROXY_PORT "; then
|
|
174
|
+
echo "Error: Port $PROXY_PORT is already in use by another process"
|
|
175
|
+
cleanup 1
|
|
176
|
+
fi
|
|
141
177
|
fi
|
|
142
178
|
fi
|
|
143
179
|
sleep 0.1
|
|
@@ -145,26 +181,23 @@ done
|
|
|
145
181
|
|
|
146
182
|
# 检查是否成功启动
|
|
147
183
|
if [ -z "${PROXY_PORT:-}" ] || ! nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
148
|
-
echo "Error: Proxy server failed to start"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
exit 1
|
|
184
|
+
echo "Error: Proxy server failed to start after 30 seconds"
|
|
185
|
+
echo "Please check the registry-proxy logs above for more details."
|
|
186
|
+
cleanup 1
|
|
152
187
|
fi
|
|
153
188
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
yarn install --registry "http://localhost:$PROXY_PORT/"
|
|
189
|
+
# 动态设置 npmRegistryServer 为代理地址
|
|
190
|
+
yarn config set npmRegistryServer "http://localhost:$PROXY_PORT/"
|
|
191
|
+
echo "Set npmRegistryServer to http://localhost:$PROXY_PORT/"
|
|
158
192
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
echo "Proxy server stopped."
|
|
193
|
+
# 使用动态代理端口运行 yarn install,并捕获错误
|
|
194
|
+
if ! yarn install; then
|
|
195
|
+
echo "Error: yarn install failed"
|
|
196
|
+
cleanup 1
|
|
197
|
+
fi
|
|
165
198
|
|
|
166
|
-
#
|
|
167
|
-
|
|
199
|
+
# 正常执行完成,调用 cleanup 并传入退出码 0
|
|
200
|
+
cleanup 0
|
|
168
201
|
```
|
|
169
202
|
|
|
170
203
|
### 5. 设置脚本权限
|
|
@@ -440,8 +473,7 @@ Create a script at `scripts/install-from-proxy-registries.sh` to automate the pr
|
|
|
440
473
|
```bash
|
|
441
474
|
#!/bin/bash
|
|
442
475
|
|
|
443
|
-
#
|
|
444
|
-
set -e # 命令失败时退出
|
|
476
|
+
# 启用严格模式,但移除 set -e,手动处理错误
|
|
445
477
|
set -u # 未定义变量时退出
|
|
446
478
|
set -o pipefail # 管道中任一命令失败时退出
|
|
447
479
|
|
|
@@ -465,46 +497,83 @@ PROJECT_ROOT=$(find_project_root)
|
|
|
465
497
|
LOCK_FILE="$PROJECT_ROOT/.registry-proxy-install.lock"
|
|
466
498
|
PORT_FILE="$PROJECT_ROOT/.registry-proxy-port"
|
|
467
499
|
|
|
468
|
-
#
|
|
500
|
+
# 检查是否已经在运行(通过锁文件)
|
|
501
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
502
|
+
echo "Custom install script is already running (lock file $LOCK_FILE exists)."
|
|
503
|
+
echo "If this is unexpected, please remove $LOCK_FILE and try again."
|
|
504
|
+
exit 0 # 内层脚本直接退出,表示任务已由外层脚本完成
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
# 创建锁文件
|
|
508
|
+
touch "$LOCK_FILE"
|
|
509
|
+
|
|
510
|
+
# 清理函数,支持不同的退出状态
|
|
511
|
+
# 参数 $1:退出状态(0 表示正常退出,1 表示异常退出)
|
|
469
512
|
cleanup() {
|
|
470
|
-
|
|
471
|
-
|
|
513
|
+
local exit_code=${1:-1} # 默认退出码为 1(异常退出)
|
|
514
|
+
|
|
515
|
+
# 显式清除 EXIT 信号的 trap,避免潜在的误解
|
|
516
|
+
trap - EXIT
|
|
517
|
+
|
|
518
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
519
|
+
echo "Cleaning up after successful execution..."
|
|
520
|
+
else
|
|
521
|
+
echo "Caught interrupt signal or error, cleaning up..."
|
|
522
|
+
fi
|
|
523
|
+
|
|
524
|
+
# 清理临时文件
|
|
525
|
+
rm -f "$LOCK_FILE" 2>/dev/null
|
|
526
|
+
rm -f "$PORT_FILE" 2>/dev/null
|
|
527
|
+
|
|
528
|
+
# 停止代理服务器
|
|
472
529
|
if [ -n "${PROXY_PID:-}" ]; then
|
|
473
|
-
echo "Stopping proxy server..."
|
|
530
|
+
echo "Stopping proxy server (PID: $PROXY_PID)..."
|
|
474
531
|
kill -TERM "$PROXY_PID" 2>/dev/null
|
|
475
|
-
wait "$PROXY_PID" 2>/dev/null ||
|
|
476
|
-
rm -f "$PORT_FILE"
|
|
532
|
+
wait "$PROXY_PID" 2>/dev/null || true
|
|
477
533
|
echo "Proxy server stopped."
|
|
478
534
|
fi
|
|
479
|
-
|
|
535
|
+
|
|
536
|
+
# 切换到项目根目录
|
|
537
|
+
cd "$PROJECT_ROOT"
|
|
538
|
+
|
|
539
|
+
# 清理 npmRegistryServer 配置
|
|
540
|
+
yarn config unset npmRegistryServer 2>/dev/null || true
|
|
541
|
+
echo "Cleared npmRegistryServer configuration"
|
|
542
|
+
|
|
543
|
+
# 根据退出状态退出
|
|
544
|
+
exit "$exit_code"
|
|
480
545
|
}
|
|
481
546
|
|
|
482
547
|
# 注册信号处理
|
|
483
|
-
trap cleanup SIGINT SIGTERM
|
|
548
|
+
trap 'cleanup 1' SIGINT SIGTERM EXIT # 异常退出时调用 cleanup,退出码为 1
|
|
484
549
|
|
|
485
|
-
#
|
|
486
|
-
|
|
487
|
-
echo "Custom install script is already running, skipping to avoid loop."
|
|
488
|
-
exit 0
|
|
489
|
-
fi
|
|
490
|
-
|
|
491
|
-
# 创建锁文件
|
|
492
|
-
touch "$LOCK_FILE"
|
|
550
|
+
# 切换到项目根目录
|
|
551
|
+
cd "$PROJECT_ROOT"
|
|
493
552
|
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
553
|
+
# 使用 yarn dlx 直接运行 registry-proxy,放入后台运行
|
|
554
|
+
REGISTRY_PROXY_VERSION="${REGISTRY_PROXY_VERSION:-latest}"
|
|
555
|
+
echo "Starting registry-proxy@$REGISTRY_PROXY_VERSION in the background (logs will be displayed below)..."
|
|
556
|
+
yarn dlx com.jimuwd.xian.registry-proxy@"$REGISTRY_PROXY_VERSION" .registry-proxy.yml .yarnrc.yml ~/.yarnrc.yml &
|
|
498
557
|
PROXY_PID=$!
|
|
499
558
|
|
|
500
|
-
# 等待代理服务器启动并写入端口,最多
|
|
501
|
-
echo "Waiting for proxy server to start..."
|
|
502
|
-
for i in {1..
|
|
559
|
+
# 等待代理服务器启动并写入端口,最多 30 秒
|
|
560
|
+
echo "Waiting for proxy server to start (up to 30 seconds)..."
|
|
561
|
+
for i in {1..300}; do # 300 次循环,每次 0.1 秒,总共 30 秒
|
|
503
562
|
if [ -f "$PORT_FILE" ]; then
|
|
504
563
|
PROXY_PORT=$(cat "$PORT_FILE")
|
|
564
|
+
if [ -z "$PROXY_PORT" ]; then
|
|
565
|
+
echo "Error: Port file $PORT_FILE is empty"
|
|
566
|
+
cleanup 1
|
|
567
|
+
fi
|
|
505
568
|
if nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
506
569
|
echo "Proxy server is ready on port $PROXY_PORT!"
|
|
507
570
|
break
|
|
571
|
+
else
|
|
572
|
+
# 检查端口是否被占用
|
|
573
|
+
if netstat -tuln 2>/dev/null | grep -q ":$PROXY_PORT "; then
|
|
574
|
+
echo "Error: Port $PROXY_PORT is already in use by another process"
|
|
575
|
+
cleanup 1
|
|
576
|
+
fi
|
|
508
577
|
fi
|
|
509
578
|
fi
|
|
510
579
|
sleep 0.1
|
|
@@ -512,26 +581,23 @@ done
|
|
|
512
581
|
|
|
513
582
|
# 检查是否成功启动
|
|
514
583
|
if [ -z "${PROXY_PORT:-}" ] || ! nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
515
|
-
echo "Error: Proxy server failed to start"
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
exit 1
|
|
584
|
+
echo "Error: Proxy server failed to start after 30 seconds"
|
|
585
|
+
echo "Please check the registry-proxy logs above for more details."
|
|
586
|
+
cleanup 1
|
|
519
587
|
fi
|
|
520
588
|
|
|
521
|
-
#
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
yarn install --registry "http://localhost:$PROXY_PORT/"
|
|
589
|
+
# 动态设置 npmRegistryServer 为代理地址
|
|
590
|
+
yarn config set npmRegistryServer "http://localhost:$PROXY_PORT/"
|
|
591
|
+
echo "Set npmRegistryServer to http://localhost:$PROXY_PORT/"
|
|
525
592
|
|
|
526
|
-
#
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
echo "Proxy server stopped."
|
|
593
|
+
# 使用动态代理端口运行 yarn install,并捕获错误
|
|
594
|
+
if ! yarn install; then
|
|
595
|
+
echo "Error: yarn install failed"
|
|
596
|
+
cleanup 1
|
|
597
|
+
fi
|
|
532
598
|
|
|
533
|
-
#
|
|
534
|
-
|
|
599
|
+
# 正常执行完成,调用 cleanup 并传入退出码 0
|
|
600
|
+
cleanup 0
|
|
535
601
|
```
|
|
536
602
|
|
|
537
603
|
### 5. Set Script Permissions
|
package/dist/index.js
CHANGED
|
@@ -1,187 +1,220 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from 'http';
|
|
3
|
-
import {
|
|
3
|
+
import { createServer as createHttpsServer } from 'https';
|
|
4
|
+
import { readFileSync, promises as fsPromises } from 'fs';
|
|
4
5
|
import { load } from 'js-yaml';
|
|
5
6
|
import fetch from 'node-fetch';
|
|
6
7
|
import { homedir } from 'os';
|
|
7
8
|
import { join, resolve } from 'path';
|
|
8
|
-
import {
|
|
9
|
+
import { URL } from 'url'; // 显式导入URL
|
|
10
|
+
const { readFile, writeFile } = fsPromises;
|
|
9
11
|
function normalizeUrl(url) {
|
|
10
|
-
|
|
12
|
+
try {
|
|
13
|
+
const urlObj = new URL(url);
|
|
14
|
+
if (urlObj.protocol === 'http:' && (urlObj.port === '80' || urlObj.port === '')) {
|
|
15
|
+
urlObj.port = '';
|
|
16
|
+
}
|
|
17
|
+
else if (urlObj.protocol === 'https:' && (urlObj.port === '443' || urlObj.port === '')) {
|
|
18
|
+
urlObj.port = '';
|
|
19
|
+
}
|
|
20
|
+
if (!urlObj.pathname.endsWith('/')) {
|
|
21
|
+
urlObj.pathname += '/';
|
|
22
|
+
}
|
|
23
|
+
return urlObj.toString();
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error(`Invalid URL: ${url}`, e);
|
|
27
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
28
|
+
}
|
|
11
29
|
}
|
|
12
30
|
function resolvePath(path) {
|
|
13
31
|
return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
14
32
|
}
|
|
15
|
-
|
|
16
|
-
const resolvedProxyPath = resolvePath(proxyConfigPath);
|
|
17
|
-
const resolvedLocalYarnPath = resolvePath(localYarnConfigPath);
|
|
18
|
-
const resolvedGlobalYarnPath = resolvePath(globalYarnConfigPath);
|
|
19
|
-
let proxyConfig = { registries: {} };
|
|
33
|
+
function removeRegistryPrefix(tarballUrl, registries) {
|
|
20
34
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
const normalizedTarball = normalizeUrl(tarballUrl);
|
|
36
|
+
const normalizedRegistries = registries
|
|
37
|
+
.map(r => normalizeUrl(r.url))
|
|
38
|
+
.sort((a, b) => b.length - a.length);
|
|
39
|
+
for (const registry of normalizedRegistries) {
|
|
40
|
+
if (normalizedTarball.startsWith(registry)) {
|
|
41
|
+
return normalizedTarball.slice(registry.length - 1) || '/';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
24
44
|
}
|
|
25
45
|
catch (e) {
|
|
26
|
-
console.error(`
|
|
27
|
-
process.exit(1);
|
|
46
|
+
console.error(`Invalid URL: ${tarballUrl}`, e);
|
|
28
47
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let localYarnConfig = { npmRegistries: {} };
|
|
48
|
+
return tarballUrl;
|
|
49
|
+
}
|
|
50
|
+
async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
|
|
51
|
+
const resolvedPath = resolvePath(proxyConfigPath);
|
|
34
52
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
const content = await readFile(resolvedPath, 'utf8');
|
|
54
|
+
const config = load(content);
|
|
55
|
+
if (!config.registries) {
|
|
56
|
+
throw new Error('Missing required "registries" field in config');
|
|
57
|
+
}
|
|
58
|
+
return config;
|
|
38
59
|
}
|
|
39
60
|
catch (e) {
|
|
40
|
-
console.
|
|
61
|
+
console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
62
|
+
process.exit(1);
|
|
41
63
|
}
|
|
42
|
-
|
|
64
|
+
}
|
|
65
|
+
async function loadYarnConfig(path) {
|
|
43
66
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
67
|
+
const content = await readFile(resolvePath(path), 'utf8');
|
|
68
|
+
return load(content);
|
|
47
69
|
}
|
|
48
70
|
catch (e) {
|
|
49
|
-
console.warn(`Failed to load ${
|
|
71
|
+
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
72
|
+
return {};
|
|
50
73
|
}
|
|
74
|
+
}
|
|
75
|
+
async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYarnConfigPath = './.yarnrc.yml', globalYarnConfigPath = join(homedir(), '.yarnrc.yml')) {
|
|
76
|
+
const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
|
|
77
|
+
loadProxyConfig(proxyConfigPath),
|
|
78
|
+
loadYarnConfig(localYarnConfigPath),
|
|
79
|
+
loadYarnConfig(globalYarnConfigPath)
|
|
80
|
+
]);
|
|
51
81
|
const registryMap = new Map();
|
|
52
82
|
for (const [url, regConfig] of Object.entries(proxyConfig.registries)) {
|
|
53
83
|
const normalizedUrl = normalizeUrl(url);
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
const registries = Array.from(registryMap.entries()).map(([url, regConfig]) => {
|
|
57
|
-
let token;
|
|
58
|
-
if (regConfig && 'npmAuthToken' in regConfig) {
|
|
59
|
-
token = regConfig.npmAuthToken?.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || regConfig.npmAuthToken;
|
|
60
|
-
}
|
|
61
|
-
const normalizedUrl = normalizeUrl(url);
|
|
62
|
-
const urlWithSlash = normalizedUrl + '/';
|
|
84
|
+
let token = regConfig?.npmAuthToken;
|
|
63
85
|
if (!token) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (!token) {
|
|
75
|
-
const globalConfig = globalYarnConfig.npmRegistries;
|
|
76
|
-
if (globalConfig?.[normalizedUrl]?.npmAuthToken) {
|
|
77
|
-
token = globalConfig[normalizedUrl].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[normalizedUrl].npmAuthToken;
|
|
78
|
-
console.log(`Token for ${url} not found in local Yarn config, using global Yarn config (normalized)`);
|
|
79
|
-
}
|
|
80
|
-
else if (globalConfig?.[urlWithSlash]?.npmAuthToken) {
|
|
81
|
-
token = globalConfig[urlWithSlash].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[urlWithSlash].npmAuthToken;
|
|
82
|
-
console.log(`Token for ${url} not found in local Yarn config, using global Yarn config (with slash)`);
|
|
86
|
+
const yarnConfigs = [localYarnConfig, globalYarnConfig];
|
|
87
|
+
for (const config of yarnConfigs) {
|
|
88
|
+
const registryConfig = config.npmRegistries?.[normalizedUrl] ||
|
|
89
|
+
config.npmRegistries?.[url];
|
|
90
|
+
if (registryConfig?.npmAuthToken) {
|
|
91
|
+
token = registryConfig.npmAuthToken;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return registries;
|
|
96
|
+
registryMap.set(normalizedUrl, { url: normalizedUrl, token });
|
|
97
|
+
}
|
|
98
|
+
return Array.from(registryMap.values());
|
|
89
99
|
}
|
|
90
100
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
91
|
-
|
|
101
|
+
const proxyConfig = await loadProxyConfig(proxyConfigPath);
|
|
92
102
|
const registries = await loadRegistries(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
res.end('
|
|
103
|
+
const basePath = proxyConfig.basePath ? `/${proxyConfig.basePath.replace(/^\/|\/$/g, '')}` : '';
|
|
104
|
+
console.log('Active registries:', registries.map(r => r.url));
|
|
105
|
+
console.log('Proxy base path:', basePath || '/');
|
|
106
|
+
console.log('HTTPS:', !!proxyConfig.https);
|
|
107
|
+
let proxyPort;
|
|
108
|
+
const requestHandler = async (req, res) => {
|
|
109
|
+
if (!req.url || !req.headers.host) {
|
|
110
|
+
res.writeHead(400).end('Invalid Request');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Handle base path
|
|
114
|
+
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
115
|
+
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
116
|
+
res.writeHead(404).end('Not Found');
|
|
101
117
|
return;
|
|
102
118
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
109
|
-
console.log(`Fetching ${targetUrl} with headers:`, JSON.stringify(headers, null, 2)); // 打印完整 headers
|
|
119
|
+
const relativePath = basePath
|
|
120
|
+
? fullUrl.pathname.slice(basePath.length)
|
|
121
|
+
: fullUrl.pathname;
|
|
122
|
+
console.log(`Proxying: ${relativePath}`);
|
|
123
|
+
const responses = await Promise.all(registries.map(async ({ url, token }) => {
|
|
110
124
|
try {
|
|
125
|
+
const targetUrl = `${url}${relativePath}${fullUrl.search || ''}`;
|
|
126
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
111
127
|
const response = await fetch(targetUrl, { headers });
|
|
112
|
-
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
const errorBody = await response.text();
|
|
115
|
-
console.log(`Error body from ${targetUrl}: ${errorBody}`);
|
|
116
|
-
}
|
|
117
|
-
return response;
|
|
128
|
+
return response.ok ? response : null;
|
|
118
129
|
}
|
|
119
130
|
catch (e) {
|
|
120
|
-
console.error(`
|
|
131
|
+
console.error(`Failed to fetch from ${url}:`, e);
|
|
121
132
|
return null;
|
|
122
133
|
}
|
|
123
|
-
});
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
}));
|
|
135
|
+
const successResponse = responses.find((r) => r !== null);
|
|
136
|
+
if (!successResponse) {
|
|
137
|
+
res.writeHead(404).end('Not Found');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
141
|
+
if (contentType.includes('application/json')) {
|
|
142
|
+
try {
|
|
143
|
+
const data = await successResponse.json();
|
|
144
|
+
if (data.versions) {
|
|
145
|
+
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
146
|
+
for (const version in data.versions) {
|
|
147
|
+
const dist = data.versions[version]?.dist;
|
|
148
|
+
if (dist?.tarball) {
|
|
149
|
+
const originalUrl = new URL(dist.tarball);
|
|
150
|
+
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
151
|
+
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
console.error('Failed to parse JSON response:', e);
|
|
159
|
+
res.writeHead(502).end('Invalid Upstream Response');
|
|
160
|
+
}
|
|
132
161
|
}
|
|
133
162
|
else {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
if (!successResponse.body) {
|
|
164
|
+
console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
|
|
165
|
+
res.writeHead(502).end('Empty Response Body');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const safeHeaders = {
|
|
169
|
+
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
170
|
+
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
171
|
+
};
|
|
172
|
+
res.writeHead(successResponse.status, safeHeaders);
|
|
173
|
+
successResponse.body.pipe(res);
|
|
137
174
|
}
|
|
138
|
-
}
|
|
175
|
+
};
|
|
176
|
+
let server;
|
|
177
|
+
if (proxyConfig.https) {
|
|
178
|
+
const { key, cert } = proxyConfig.https;
|
|
179
|
+
try {
|
|
180
|
+
await fsPromises.access(resolvePath(key));
|
|
181
|
+
await fsPromises.access(resolvePath(cert));
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const httpsOptions = {
|
|
188
|
+
key: readFileSync(resolvePath(key)),
|
|
189
|
+
cert: readFileSync(resolvePath(cert)),
|
|
190
|
+
};
|
|
191
|
+
server = createHttpsServer(httpsOptions, requestHandler);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
server = createServer(requestHandler);
|
|
195
|
+
}
|
|
139
196
|
return new Promise((resolve, reject) => {
|
|
197
|
+
server.on('error', (err) => {
|
|
198
|
+
if (err.code === 'EADDRINUSE') {
|
|
199
|
+
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
reject(err);
|
|
203
|
+
});
|
|
140
204
|
server.listen(port, () => {
|
|
141
205
|
const address = server.address();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
if (typeof address === 'string') {
|
|
148
|
-
console.error('Server bound to a path (e.g., Unix socket), which is not supported');
|
|
149
|
-
reject(new Error('Server bound to a path, expected a TCP port'));
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const addressInfo = address;
|
|
153
|
-
const actualPort = addressInfo.port;
|
|
154
|
-
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
155
|
-
const portFilePath = join(projectRoot, '.registry-proxy-port');
|
|
156
|
-
console.log(`Proxy server started at http://localhost:${actualPort}`);
|
|
157
|
-
writeFileSync(portFilePath, actualPort.toString(), 'utf8');
|
|
206
|
+
proxyPort = address.port;
|
|
207
|
+
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
208
|
+
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
209
|
+
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
158
210
|
resolve(server);
|
|
159
211
|
});
|
|
160
|
-
process.on('SIGTERM', () => {
|
|
161
|
-
console.log('Received SIGTERM, shutting down...');
|
|
162
|
-
server.close((err) => {
|
|
163
|
-
if (err) {
|
|
164
|
-
console.error('Error closing server:', err.message);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
console.log('Server closed.');
|
|
168
|
-
process.exit(0);
|
|
169
|
-
});
|
|
170
|
-
setTimeout(() => {
|
|
171
|
-
console.error('Server did not close in time, forcing exit...');
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}, 5000);
|
|
174
|
-
});
|
|
175
212
|
});
|
|
176
213
|
}
|
|
177
214
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const port = parseInt(process.argv[5], 10) || 0;
|
|
182
|
-
console.log(`CLI: proxyConfigPath=${proxyConfigPath || './.registry-proxy.yml'}, localYarnConfigPath=${localYarnConfigPath || './.yarnrc.yml'}, globalYarnConfigPath=${globalYarnConfigPath || join(homedir(), '.yarnrc.yml')}, port=${port || 'dynamic'}`);
|
|
183
|
-
startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port).catch(err => {
|
|
184
|
-
console.error('Startup failed:', err.message);
|
|
215
|
+
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
216
|
+
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
217
|
+
console.error('Failed to start server:', err);
|
|
185
218
|
process.exit(1);
|
|
186
219
|
});
|
|
187
220
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,213 +1,264 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createServer, Server } from 'http';
|
|
2
|
+
import { createServer, Server as HttpServer } from 'http';
|
|
3
|
+
import { createServer as createHttpsServer, Server as HttpsServer } from 'https';
|
|
4
|
+
import { readFileSync, promises as fsPromises } from 'fs';
|
|
3
5
|
import { AddressInfo } from 'net';
|
|
4
|
-
import { readFile } from 'fs/promises';
|
|
5
6
|
import { load } from 'js-yaml';
|
|
6
7
|
import fetch, { Response } from 'node-fetch';
|
|
7
8
|
import { homedir } from 'os';
|
|
8
9
|
import { join, resolve } from 'path';
|
|
9
|
-
import {
|
|
10
|
+
import { URL } from 'url'; // 显式导入URL
|
|
11
|
+
|
|
12
|
+
const { readFile, writeFile } = fsPromises;
|
|
10
13
|
|
|
11
14
|
interface RegistryConfig { npmAuthToken?: string; }
|
|
12
|
-
interface
|
|
15
|
+
interface HttpsConfig { key: string; cert: string; }
|
|
16
|
+
interface ProxyConfig {
|
|
17
|
+
registries: Record<string, RegistryConfig | null>;
|
|
18
|
+
https?: HttpsConfig;
|
|
19
|
+
basePath?: string;
|
|
20
|
+
}
|
|
13
21
|
interface YarnConfig { npmRegistries?: Record<string, RegistryConfig | null>; }
|
|
22
|
+
interface RegistryInfo { url: string; token?: string; }
|
|
23
|
+
interface PackageVersion { dist?: { tarball?: string }; }
|
|
24
|
+
interface PackageData { versions?: Record<string, PackageVersion>; }
|
|
14
25
|
|
|
15
26
|
function normalizeUrl(url: string): string {
|
|
16
|
-
|
|
27
|
+
try {
|
|
28
|
+
const urlObj = new URL(url);
|
|
29
|
+
if (urlObj.protocol === 'http:' && (urlObj.port === '80' || urlObj.port === '')) {
|
|
30
|
+
urlObj.port = '';
|
|
31
|
+
} else if (urlObj.protocol === 'https:' && (urlObj.port === '443' || urlObj.port === '')) {
|
|
32
|
+
urlObj.port = '';
|
|
33
|
+
}
|
|
34
|
+
if (!urlObj.pathname.endsWith('/')) {
|
|
35
|
+
urlObj.pathname += '/';
|
|
36
|
+
}
|
|
37
|
+
return urlObj.toString();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(`Invalid URL: ${url}`, e);
|
|
40
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
41
|
+
}
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
function resolvePath(path: string): string {
|
|
20
45
|
return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
21
46
|
}
|
|
22
47
|
|
|
23
|
-
|
|
24
|
-
const resolvedProxyPath = resolvePath(proxyConfigPath);
|
|
25
|
-
const resolvedLocalYarnPath = resolvePath(localYarnConfigPath);
|
|
26
|
-
const resolvedGlobalYarnPath = resolvePath(globalYarnConfigPath);
|
|
27
|
-
|
|
28
|
-
let proxyConfig: ProxyConfig = { registries: {} };
|
|
48
|
+
function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
|
|
29
49
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.error(`Failed to load ${resolvedProxyPath}: ${(e as Error).message}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
50
|
+
const normalizedTarball = normalizeUrl(tarballUrl);
|
|
51
|
+
const normalizedRegistries = registries
|
|
52
|
+
.map(r => normalizeUrl(r.url))
|
|
53
|
+
.sort((a, b) => b.length - a.length);
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
for (const registry of normalizedRegistries) {
|
|
56
|
+
if (normalizedTarball.startsWith(registry)) {
|
|
57
|
+
return normalizedTarball.slice(registry.length - 1) || '/';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`Invalid URL: ${tarballUrl}`, e);
|
|
41
62
|
}
|
|
63
|
+
return tarballUrl;
|
|
64
|
+
}
|
|
42
65
|
|
|
43
|
-
|
|
66
|
+
async function loadProxyConfig(
|
|
67
|
+
proxyConfigPath = './.registry-proxy.yml'
|
|
68
|
+
): Promise<ProxyConfig> {
|
|
69
|
+
const resolvedPath = resolvePath(proxyConfigPath);
|
|
44
70
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
const content = await readFile(resolvedPath, 'utf8');
|
|
72
|
+
const config = load(content) as ProxyConfig;
|
|
73
|
+
if (!config.registries) {
|
|
74
|
+
throw new Error('Missing required "registries" field in config');
|
|
75
|
+
}
|
|
76
|
+
return config;
|
|
48
77
|
} catch (e) {
|
|
49
|
-
console.
|
|
78
|
+
console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
79
|
+
process.exit(1);
|
|
50
80
|
}
|
|
81
|
+
}
|
|
51
82
|
|
|
52
|
-
|
|
83
|
+
async function loadYarnConfig(path: string): Promise<YarnConfig> {
|
|
53
84
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
85
|
+
const content = await readFile(resolvePath(path), 'utf8');
|
|
86
|
+
return load(content) as YarnConfig;
|
|
57
87
|
} catch (e) {
|
|
58
|
-
console.warn(`Failed to load ${
|
|
88
|
+
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
89
|
+
return {};
|
|
59
90
|
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadRegistries(
|
|
94
|
+
proxyConfigPath = './.registry-proxy.yml',
|
|
95
|
+
localYarnConfigPath = './.yarnrc.yml',
|
|
96
|
+
globalYarnConfigPath = join(homedir(), '.yarnrc.yml')
|
|
97
|
+
): Promise<RegistryInfo[]> {
|
|
98
|
+
const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
|
|
99
|
+
loadProxyConfig(proxyConfigPath),
|
|
100
|
+
loadYarnConfig(localYarnConfigPath),
|
|
101
|
+
loadYarnConfig(globalYarnConfigPath)
|
|
102
|
+
]);
|
|
60
103
|
|
|
61
|
-
const registryMap = new Map<string,
|
|
104
|
+
const registryMap = new Map<string, RegistryInfo>();
|
|
62
105
|
for (const [url, regConfig] of Object.entries(proxyConfig.registries)) {
|
|
63
106
|
const normalizedUrl = normalizeUrl(url);
|
|
64
|
-
|
|
107
|
+
let token = regConfig?.npmAuthToken;
|
|
108
|
+
|
|
109
|
+
if (!token) {
|
|
110
|
+
const yarnConfigs = [localYarnConfig, globalYarnConfig];
|
|
111
|
+
for (const config of yarnConfigs) {
|
|
112
|
+
const registryConfig = config.npmRegistries?.[normalizedUrl] ||
|
|
113
|
+
config.npmRegistries?.[url];
|
|
114
|
+
if (registryConfig?.npmAuthToken) {
|
|
115
|
+
token = registryConfig.npmAuthToken;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
registryMap.set(normalizedUrl, { url: normalizedUrl, token });
|
|
65
121
|
}
|
|
122
|
+
return Array.from(registryMap.values());
|
|
123
|
+
}
|
|
66
124
|
|
|
67
|
-
|
|
68
|
-
|
|
125
|
+
export async function startProxyServer(
|
|
126
|
+
proxyConfigPath?: string,
|
|
127
|
+
localYarnConfigPath?: string,
|
|
128
|
+
globalYarnConfigPath?: string,
|
|
129
|
+
port: number = 0
|
|
130
|
+
): Promise<HttpServer | HttpsServer> {
|
|
131
|
+
const proxyConfig = await loadProxyConfig(proxyConfigPath);
|
|
132
|
+
const registries = await loadRegistries(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
133
|
+
const basePath = proxyConfig.basePath ? `/${proxyConfig.basePath.replace(/^\/|\/$/g, '')}` : '';
|
|
69
134
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
135
|
+
console.log('Active registries:', registries.map(r => r.url));
|
|
136
|
+
console.log('Proxy base path:', basePath || '/');
|
|
137
|
+
console.log('HTTPS:', !!proxyConfig.https);
|
|
73
138
|
|
|
74
|
-
|
|
75
|
-
const urlWithSlash = normalizedUrl + '/';
|
|
139
|
+
let proxyPort: number;
|
|
76
140
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.log(`Token for ${url} not found in ${resolvedProxyPath}, using local Yarn config (normalized)`);
|
|
82
|
-
} else if (localConfig?.[urlWithSlash]?.npmAuthToken) {
|
|
83
|
-
token = localConfig[urlWithSlash].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || localConfig[urlWithSlash].npmAuthToken;
|
|
84
|
-
console.log(`Token for ${url} not found in ${resolvedProxyPath}, using local Yarn config (with slash)`);
|
|
85
|
-
}
|
|
141
|
+
const requestHandler = async (req: any, res: any) => {
|
|
142
|
+
if (!req.url || !req.headers.host) {
|
|
143
|
+
res.writeHead(400).end('Invalid Request');
|
|
144
|
+
return;
|
|
86
145
|
}
|
|
87
146
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} else if (globalConfig?.[urlWithSlash]?.npmAuthToken) {
|
|
94
|
-
token = globalConfig[urlWithSlash].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[urlWithSlash].npmAuthToken;
|
|
95
|
-
console.log(`Token for ${url} not found in local Yarn config, using global Yarn config (with slash)`);
|
|
96
|
-
}
|
|
147
|
+
// Handle base path
|
|
148
|
+
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
149
|
+
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
150
|
+
res.writeHead(404).end('Not Found');
|
|
151
|
+
return;
|
|
97
152
|
}
|
|
98
153
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
const relativePath = basePath
|
|
155
|
+
? fullUrl.pathname.slice(basePath.length)
|
|
156
|
+
: fullUrl.pathname;
|
|
102
157
|
|
|
103
|
-
|
|
104
|
-
}
|
|
158
|
+
console.log(`Proxying: ${relativePath}`);
|
|
105
159
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
160
|
+
const responses = await Promise.all(
|
|
161
|
+
registries.map(async ({ url, token }) => {
|
|
162
|
+
try {
|
|
163
|
+
const targetUrl = `${url}${relativePath}${fullUrl.search || ''}`;
|
|
164
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
165
|
+
const response = await fetch(targetUrl, { headers });
|
|
166
|
+
return response.ok ? response : null;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error(`Failed to fetch from ${url}:`, e);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
);
|
|
112
173
|
|
|
113
|
-
|
|
114
|
-
if (!
|
|
115
|
-
|
|
116
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
117
|
-
res.end('Bad Request');
|
|
174
|
+
const successResponse = responses.find((r): r is Response => r !== null);
|
|
175
|
+
if (!successResponse) {
|
|
176
|
+
res.writeHead(404).end('Not Found');
|
|
118
177
|
return;
|
|
119
178
|
}
|
|
120
179
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const pathname = fullUrl.pathname;
|
|
124
|
-
|
|
125
|
-
const fetchPromises = registries.map(async ({ url: registry, token }) => {
|
|
126
|
-
const targetUrl = `${registry}${pathname}`;
|
|
127
|
-
const headers: Record<string, string> | undefined = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
128
|
-
console.log(`Fetching ${targetUrl} with headers:`, JSON.stringify(headers, null, 2)); // 打印完整 headers
|
|
180
|
+
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
181
|
+
if (contentType.includes('application/json')) {
|
|
129
182
|
try {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
183
|
+
const data = await successResponse.json() as PackageData;
|
|
184
|
+
if (data.versions) {
|
|
185
|
+
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
186
|
+
for (const version in data.versions) {
|
|
187
|
+
const dist = data.versions[version]?.dist;
|
|
188
|
+
if (dist?.tarball) {
|
|
189
|
+
const originalUrl = new URL(dist.tarball);
|
|
190
|
+
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
191
|
+
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
135
194
|
}
|
|
136
|
-
|
|
195
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));
|
|
137
196
|
} catch (e) {
|
|
138
|
-
console.error(
|
|
139
|
-
|
|
197
|
+
console.error('Failed to parse JSON response:', e);
|
|
198
|
+
res.writeHead(502).end('Invalid Upstream Response');
|
|
140
199
|
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const responses = await Promise.all(fetchPromises);
|
|
144
|
-
const successResponse = responses.find((r: Response | null) => r?.ok);
|
|
145
|
-
|
|
146
|
-
if (successResponse) {
|
|
147
|
-
console.log(`Forwarding successful response from ${successResponse.url}: ${successResponse.status} ${successResponse.statusText}`);
|
|
148
|
-
res.writeHead(successResponse.status, {
|
|
149
|
-
'Content-Type': successResponse.headers.get('Content-Type') || 'application/octet-stream',
|
|
150
|
-
});
|
|
151
|
-
successResponse.body?.pipe(res);
|
|
152
200
|
} else {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
return new Promise((resolve, reject) => {
|
|
160
|
-
server.listen(port, () => {
|
|
161
|
-
const address: AddressInfo | string | null = server.address();
|
|
162
|
-
if (!address) {
|
|
163
|
-
console.error('Failed to get server address: address is null');
|
|
164
|
-
reject(new Error('Failed to get server address: address is null'));
|
|
201
|
+
if (!successResponse.body) {
|
|
202
|
+
console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
|
|
203
|
+
res.writeHead(502).end('Empty Response Body');
|
|
165
204
|
return;
|
|
166
205
|
}
|
|
206
|
+
const safeHeaders = {
|
|
207
|
+
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
208
|
+
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
209
|
+
};
|
|
210
|
+
res.writeHead(successResponse.status, safeHeaders);
|
|
211
|
+
successResponse.body.pipe(res);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
167
214
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
server.close((err) => {
|
|
187
|
-
if (err) {
|
|
188
|
-
console.error('Error closing server:', err.message);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
console.log('Server closed.');
|
|
192
|
-
process.exit(0);
|
|
193
|
-
});
|
|
215
|
+
let server: HttpServer | HttpsServer;
|
|
216
|
+
if (proxyConfig.https) {
|
|
217
|
+
const { key, cert } = proxyConfig.https;
|
|
218
|
+
try {
|
|
219
|
+
await fsPromises.access(resolvePath(key));
|
|
220
|
+
await fsPromises.access(resolvePath(cert));
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const httpsOptions = {
|
|
226
|
+
key: readFileSync(resolvePath(key)),
|
|
227
|
+
cert: readFileSync(resolvePath(cert)),
|
|
228
|
+
};
|
|
229
|
+
server = createHttpsServer(httpsOptions, requestHandler);
|
|
230
|
+
} else {
|
|
231
|
+
server = createServer(requestHandler);
|
|
232
|
+
}
|
|
194
233
|
|
|
195
|
-
|
|
196
|
-
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
236
|
+
if (err.code === 'EADDRINUSE') {
|
|
237
|
+
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
197
238
|
process.exit(1);
|
|
198
|
-
}
|
|
239
|
+
}
|
|
240
|
+
reject(err);
|
|
241
|
+
});
|
|
242
|
+
server.listen(port, () => {
|
|
243
|
+
const address = server.address() as AddressInfo;
|
|
244
|
+
proxyPort = address.port;
|
|
245
|
+
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
246
|
+
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
247
|
+
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
248
|
+
resolve(server);
|
|
199
249
|
});
|
|
200
250
|
});
|
|
201
251
|
}
|
|
202
252
|
|
|
203
253
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
254
|
+
const [,, configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
255
|
+
startProxyServer(
|
|
256
|
+
configPath,
|
|
257
|
+
localYarnPath,
|
|
258
|
+
globalYarnPath,
|
|
259
|
+
parseInt(port, 10) || 0
|
|
260
|
+
).catch(err => {
|
|
261
|
+
console.error('Failed to start server:', err);
|
|
211
262
|
process.exit(1);
|
|
212
263
|
});
|
|
213
264
|
}
|