com.jimuwd.xian.registry-proxy 1.0.11 → 1.0.13
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 +164 -155
- package/package.json +1 -1
- package/src/index.ts +203 -179
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,214 +1,223 @@
|
|
|
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
|
-
// 原有逻辑保持不变
|
|
17
|
-
const resolvedProxyPath = resolvePath(proxyConfigPath);
|
|
18
|
-
const resolvedLocalYarnPath = resolvePath(localYarnConfigPath);
|
|
19
|
-
const resolvedGlobalYarnPath = resolvePath(globalYarnConfigPath);
|
|
20
|
-
let proxyConfig = { registries: {} };
|
|
33
|
+
function removeRegistryPrefix(tarballUrl, registries) {
|
|
21
34
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
44
|
}
|
|
26
45
|
catch (e) {
|
|
27
|
-
console.error(`
|
|
28
|
-
process.exit(1);
|
|
46
|
+
console.error(`Invalid URL: ${tarballUrl}`, e);
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let localYarnConfig = { npmRegistries: {} };
|
|
48
|
+
return tarballUrl;
|
|
49
|
+
}
|
|
50
|
+
async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
|
|
51
|
+
const resolvedPath = resolvePath(proxyConfigPath);
|
|
35
52
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
39
59
|
}
|
|
40
60
|
catch (e) {
|
|
41
|
-
console.
|
|
61
|
+
console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
62
|
+
process.exit(1);
|
|
42
63
|
}
|
|
43
|
-
|
|
64
|
+
}
|
|
65
|
+
async function loadYarnConfig(path) {
|
|
44
66
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
67
|
+
const content = await readFile(resolvePath(path), 'utf8');
|
|
68
|
+
return load(content);
|
|
48
69
|
}
|
|
49
70
|
catch (e) {
|
|
50
|
-
console.warn(`Failed to load ${
|
|
71
|
+
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
72
|
+
return {};
|
|
51
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
|
+
]);
|
|
52
81
|
const registryMap = new Map();
|
|
53
82
|
for (const [url, regConfig] of Object.entries(proxyConfig.registries)) {
|
|
54
83
|
const normalizedUrl = normalizeUrl(url);
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
const registries = Array.from(registryMap.entries()).map(([url, regConfig]) => {
|
|
58
|
-
let token;
|
|
59
|
-
if (regConfig && 'npmAuthToken' in regConfig) {
|
|
60
|
-
token = regConfig.npmAuthToken?.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || regConfig.npmAuthToken;
|
|
61
|
-
}
|
|
62
|
-
const normalizedUrl = normalizeUrl(url);
|
|
63
|
-
const urlWithSlash = normalizedUrl + '/';
|
|
84
|
+
let token = regConfig?.npmAuthToken;
|
|
64
85
|
if (!token) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (!token) {
|
|
76
|
-
const globalConfig = globalYarnConfig.npmRegistries;
|
|
77
|
-
if (globalConfig?.[normalizedUrl]?.npmAuthToken) {
|
|
78
|
-
token = globalConfig[normalizedUrl].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[normalizedUrl].npmAuthToken;
|
|
79
|
-
console.log(`Token for ${url} not found in local Yarn config, using global Yarn config (normalized)`);
|
|
80
|
-
}
|
|
81
|
-
else if (globalConfig?.[urlWithSlash]?.npmAuthToken) {
|
|
82
|
-
token = globalConfig[urlWithSlash].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[urlWithSlash].npmAuthToken;
|
|
83
|
-
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
|
+
}
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return registries;
|
|
96
|
+
registryMap.set(normalizedUrl, { url: normalizedUrl, token });
|
|
97
|
+
}
|
|
98
|
+
return Array.from(registryMap.values());
|
|
90
99
|
}
|
|
91
100
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
92
|
-
|
|
101
|
+
const proxyConfig = await loadProxyConfig(proxyConfigPath);
|
|
93
102
|
const registries = await loadRegistries(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
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');
|
|
102
111
|
return;
|
|
103
112
|
}
|
|
104
|
-
const fullUrl = new URL(req.url,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
114
|
+
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
115
|
+
res.writeHead(404).end('Not Found');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const relativePath = basePath
|
|
119
|
+
? fullUrl.pathname.slice(basePath.length)
|
|
120
|
+
: fullUrl.pathname;
|
|
121
|
+
console.log(`Proxying: ${relativePath}`);
|
|
122
|
+
const responses = await Promise.all(registries.map(async ({ url, token }) => {
|
|
111
123
|
try {
|
|
124
|
+
const cleanRelativePath = relativePath.replace(/\/+$/, '');
|
|
125
|
+
const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
|
|
126
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
127
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
112
128
|
const response = await fetch(targetUrl, { headers });
|
|
113
|
-
console.log(`Response from ${
|
|
114
|
-
|
|
115
|
-
const errorBody = await response.text();
|
|
116
|
-
console.log(`Error body from ${targetUrl}: ${errorBody}`);
|
|
117
|
-
}
|
|
118
|
-
return response;
|
|
129
|
+
console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
|
|
130
|
+
return response.ok ? response : null;
|
|
119
131
|
}
|
|
120
132
|
catch (e) {
|
|
121
|
-
console.error(`
|
|
133
|
+
console.error(`Failed to fetch from ${url}:`, e);
|
|
122
134
|
return null;
|
|
123
135
|
}
|
|
124
|
-
});
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const tarballPath =
|
|
142
|
-
dist.tarball = `${
|
|
143
|
-
console.log(`Rewrote tarball URL from ${originalTarball} to ${dist.tarball}`);
|
|
136
|
+
}));
|
|
137
|
+
const successResponse = responses.find((r) => r !== null);
|
|
138
|
+
if (!successResponse) {
|
|
139
|
+
console.error(`All registries failed for ${relativePath}`);
|
|
140
|
+
res.writeHead(404).end('Not Found - All upstream registries failed');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
144
|
+
if (contentType.includes('application/json')) {
|
|
145
|
+
try {
|
|
146
|
+
const data = await successResponse.json();
|
|
147
|
+
if (data.versions) {
|
|
148
|
+
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
149
|
+
for (const version in data.versions) {
|
|
150
|
+
const dist = data.versions[version]?.dist;
|
|
151
|
+
if (dist?.tarball) {
|
|
152
|
+
const originalUrl = new URL(dist.tarball);
|
|
153
|
+
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
154
|
+
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
|
-
|
|
148
|
-
res.writeHead(successResponse.status, { 'Content-Type': 'application/json' });
|
|
149
|
-
res.end(JSON.stringify(jsonData));
|
|
158
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));
|
|
150
159
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
res.writeHead(
|
|
154
|
-
'Content-Type': contentType,
|
|
155
|
-
'Content-Length': successResponse.headers.get('Content-Length') || undefined,
|
|
156
|
-
});
|
|
157
|
-
successResponse.body?.pipe(res);
|
|
160
|
+
catch (e) {
|
|
161
|
+
console.error('Failed to parse JSON response:', e);
|
|
162
|
+
res.writeHead(502).end('Invalid Upstream Response');
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
else {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
if (!successResponse.body) {
|
|
167
|
+
console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
|
|
168
|
+
res.writeHead(502).end('Empty Response Body');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const safeHeaders = {
|
|
172
|
+
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
173
|
+
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
174
|
+
};
|
|
175
|
+
res.writeHead(successResponse.status, safeHeaders);
|
|
176
|
+
successResponse.body.pipe(res);
|
|
164
177
|
}
|
|
165
|
-
}
|
|
178
|
+
};
|
|
179
|
+
let server;
|
|
180
|
+
if (proxyConfig.https) {
|
|
181
|
+
const { key, cert } = proxyConfig.https;
|
|
182
|
+
try {
|
|
183
|
+
await fsPromises.access(resolvePath(key));
|
|
184
|
+
await fsPromises.access(resolvePath(cert));
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const httpsOptions = {
|
|
191
|
+
key: readFileSync(resolvePath(key)),
|
|
192
|
+
cert: readFileSync(resolvePath(cert)),
|
|
193
|
+
};
|
|
194
|
+
server = createHttpsServer(httpsOptions, requestHandler);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
server = createServer(requestHandler);
|
|
198
|
+
}
|
|
166
199
|
return new Promise((resolve, reject) => {
|
|
200
|
+
server.on('error', (err) => {
|
|
201
|
+
if (err.code === 'EADDRINUSE') {
|
|
202
|
+
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
reject(err);
|
|
206
|
+
});
|
|
167
207
|
server.listen(port, () => {
|
|
168
208
|
const address = server.address();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
if (typeof address === 'string') {
|
|
175
|
-
console.error('Server bound to a path (e.g., Unix socket), which is not supported');
|
|
176
|
-
reject(new Error('Server bound to a path, expected a TCP port'));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const addressInfo = address;
|
|
180
|
-
const actualPort = addressInfo.port;
|
|
181
|
-
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
182
|
-
const portFilePath = join(projectRoot, '.registry-proxy-port');
|
|
183
|
-
console.log(`Proxy server started at http://localhost:${actualPort}`);
|
|
184
|
-
writeFileSync(portFilePath, actualPort.toString(), 'utf8');
|
|
209
|
+
proxyPort = address.port;
|
|
210
|
+
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
211
|
+
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
212
|
+
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
185
213
|
resolve(server);
|
|
186
214
|
});
|
|
187
|
-
process.on('SIGTERM', () => {
|
|
188
|
-
console.log('Received SIGTERM, shutting down...');
|
|
189
|
-
server.close((err) => {
|
|
190
|
-
if (err) {
|
|
191
|
-
console.error('Error closing server:', err.message);
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
console.log('Server closed.');
|
|
195
|
-
process.exit(0);
|
|
196
|
-
});
|
|
197
|
-
setTimeout(() => {
|
|
198
|
-
console.error('Server did not close in time, forcing exit...');
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}, 5000);
|
|
201
|
-
});
|
|
202
215
|
});
|
|
203
216
|
}
|
|
204
217
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const port = parseInt(process.argv[5], 10) || 0;
|
|
209
|
-
console.log(`CLI: proxyConfigPath=${proxyConfigPath || './.registry-proxy.yml'}, localYarnConfigPath=${localYarnConfigPath || './.yarnrc.yml'}, globalYarnConfigPath=${globalYarnConfigPath || join(homedir(), '.yarnrc.yml')}, port=${port || 'dynamic'}`);
|
|
210
|
-
startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port).catch(err => {
|
|
211
|
-
console.error('Startup failed:', err.message);
|
|
218
|
+
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
219
|
+
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
220
|
+
console.error('Failed to start server:', err);
|
|
212
221
|
process.exit(1);
|
|
213
222
|
});
|
|
214
223
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,242 +1,266 @@
|
|
|
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
|
-
// 原有逻辑保持不变
|
|
25
|
-
const resolvedProxyPath = resolvePath(proxyConfigPath);
|
|
26
|
-
const resolvedLocalYarnPath = resolvePath(localYarnConfigPath);
|
|
27
|
-
const resolvedGlobalYarnPath = resolvePath(globalYarnConfigPath);
|
|
28
|
-
|
|
29
|
-
let proxyConfig: ProxyConfig = { registries: {} };
|
|
48
|
+
function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
|
|
30
49
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
const normalizedTarball = normalizeUrl(tarballUrl);
|
|
51
|
+
const normalizedRegistries = registries
|
|
52
|
+
.map(r => normalizeUrl(r.url))
|
|
53
|
+
.sort((a, b) => b.length - a.length);
|
|
54
|
+
|
|
55
|
+
for (const registry of normalizedRegistries) {
|
|
56
|
+
if (normalizedTarball.startsWith(registry)) {
|
|
57
|
+
return normalizedTarball.slice(registry.length - 1) || '/';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
34
60
|
} catch (e) {
|
|
35
|
-
console.error(`
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!proxyConfig.registries || !Object.keys(proxyConfig.registries).length) {
|
|
40
|
-
console.error(`No registries found in ${resolvedProxyPath}`);
|
|
41
|
-
process.exit(1);
|
|
61
|
+
console.error(`Invalid URL: ${tarballUrl}`, e);
|
|
42
62
|
}
|
|
63
|
+
return tarballUrl;
|
|
64
|
+
}
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
async function loadProxyConfig(
|
|
67
|
+
proxyConfigPath = './.registry-proxy.yml'
|
|
68
|
+
): Promise<ProxyConfig> {
|
|
69
|
+
const resolvedPath = resolvePath(proxyConfigPath);
|
|
45
70
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
49
77
|
} catch (e) {
|
|
50
|
-
console.
|
|
78
|
+
console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
79
|
+
process.exit(1);
|
|
51
80
|
}
|
|
81
|
+
}
|
|
52
82
|
|
|
53
|
-
|
|
83
|
+
async function loadYarnConfig(path: string): Promise<YarnConfig> {
|
|
54
84
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
85
|
+
const content = await readFile(resolvePath(path), 'utf8');
|
|
86
|
+
return load(content) as YarnConfig;
|
|
58
87
|
} catch (e) {
|
|
59
|
-
console.warn(`Failed to load ${
|
|
88
|
+
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
89
|
+
return {};
|
|
60
90
|
}
|
|
91
|
+
}
|
|
61
92
|
|
|
62
|
-
|
|
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
|
+
]);
|
|
103
|
+
|
|
104
|
+
const registryMap = new Map<string, RegistryInfo>();
|
|
63
105
|
for (const [url, regConfig] of Object.entries(proxyConfig.registries)) {
|
|
64
106
|
const normalizedUrl = normalizeUrl(url);
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const registries = Array.from(registryMap.entries()).map(([url, regConfig]) => {
|
|
69
|
-
let token: string | undefined;
|
|
70
|
-
|
|
71
|
-
if (regConfig && 'npmAuthToken' in regConfig) {
|
|
72
|
-
token = regConfig.npmAuthToken?.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || regConfig.npmAuthToken;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const normalizedUrl = normalizeUrl(url);
|
|
76
|
-
const urlWithSlash = normalizedUrl + '/';
|
|
107
|
+
let token = regConfig?.npmAuthToken;
|
|
77
108
|
|
|
78
109
|
if (!token) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!token) {
|
|
90
|
-
const globalConfig = globalYarnConfig.npmRegistries;
|
|
91
|
-
if (globalConfig?.[normalizedUrl]?.npmAuthToken) {
|
|
92
|
-
token = globalConfig[normalizedUrl].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[normalizedUrl].npmAuthToken;
|
|
93
|
-
console.log(`Token for ${url} not found in local Yarn config, using global Yarn config (normalized)`);
|
|
94
|
-
} else if (globalConfig?.[urlWithSlash]?.npmAuthToken) {
|
|
95
|
-
token = globalConfig[urlWithSlash].npmAuthToken.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalConfig[urlWithSlash].npmAuthToken;
|
|
96
|
-
console.log(`Token for ${url} not found in local Yarn config, using global Yarn config (with slash)`);
|
|
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
|
+
}
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return registries;
|
|
120
|
+
registryMap.set(normalizedUrl, { url: normalizedUrl, token });
|
|
121
|
+
}
|
|
122
|
+
return Array.from(registryMap.values());
|
|
105
123
|
}
|
|
106
124
|
|
|
107
|
-
export async function startProxyServer(
|
|
108
|
-
|
|
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);
|
|
109
132
|
const registries = await loadRegistries(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
110
|
-
|
|
111
|
-
console.log(`Registry: ${url}, Token: ${token ? 'present' : 'missing'}`);
|
|
112
|
-
});
|
|
133
|
+
const basePath = proxyConfig.basePath ? `/${proxyConfig.basePath.replace(/^\/|\/$/g, '')}` : '';
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
console.log('Active registries:', registries.map(r => r.url));
|
|
136
|
+
console.log('Proxy base path:', basePath || '/');
|
|
137
|
+
console.log('HTTPS:', !!proxyConfig.https);
|
|
138
|
+
|
|
139
|
+
let proxyPort: number;
|
|
140
|
+
|
|
141
|
+
const requestHandler = async (req: any, res: any) => {
|
|
142
|
+
if (!req.url || !req.headers.host) {
|
|
143
|
+
res.writeHead(400).end('Invalid Request');
|
|
119
144
|
return;
|
|
120
145
|
}
|
|
121
146
|
|
|
122
|
-
const fullUrl = new URL(req.url,
|
|
123
|
-
|
|
124
|
-
|
|
147
|
+
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
148
|
+
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
149
|
+
res.writeHead(404).end('Not Found');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
125
152
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
153
|
+
const relativePath = basePath
|
|
154
|
+
? fullUrl.pathname.slice(basePath.length)
|
|
155
|
+
: fullUrl.pathname;
|
|
156
|
+
console.log(`Proxying: ${relativePath}`);
|
|
157
|
+
|
|
158
|
+
const responses = await Promise.all(
|
|
159
|
+
registries.map(async ({ url, token }) => {
|
|
160
|
+
try {
|
|
161
|
+
const cleanRelativePath = relativePath.replace(/\/+$/, '');
|
|
162
|
+
const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
|
|
163
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
164
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
165
|
+
const response = await fetch(targetUrl, { headers });
|
|
166
|
+
console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
|
|
167
|
+
return response.ok ? response : null;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error(`Failed to fetch from ${url}:`, e);
|
|
170
|
+
return null;
|
|
136
171
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.error(`Fetch failed for ${targetUrl}: ${(e as Error).message}`);
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
});
|
|
172
|
+
})
|
|
173
|
+
);
|
|
143
174
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const tarballPath = new URL(originalTarball).pathname; // 提取路径部分
|
|
164
|
-
dist.tarball = `${proxyBaseUrl}${tarballPath}`;
|
|
165
|
-
console.log(`Rewrote tarball URL from ${originalTarball} to ${dist.tarball}`);
|
|
175
|
+
const successResponse = responses.find((r): r is Response => r !== null);
|
|
176
|
+
if (!successResponse) {
|
|
177
|
+
console.error(`All registries failed for ${relativePath}`);
|
|
178
|
+
res.writeHead(404).end('Not Found - All upstream registries failed');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
183
|
+
if (contentType.includes('application/json')) {
|
|
184
|
+
try {
|
|
185
|
+
const data = await successResponse.json() as PackageData;
|
|
186
|
+
if (data.versions) {
|
|
187
|
+
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
188
|
+
for (const version in data.versions) {
|
|
189
|
+
const dist = data.versions[version]?.dist;
|
|
190
|
+
if (dist?.tarball) {
|
|
191
|
+
const originalUrl = new URL(dist.tarball);
|
|
192
|
+
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
193
|
+
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
166
194
|
}
|
|
167
195
|
}
|
|
168
196
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
res.
|
|
173
|
-
} else {
|
|
174
|
-
// 非 JSON 响应(例如 tarball 文件),直接转发
|
|
175
|
-
res.writeHead(successResponse.status, {
|
|
176
|
-
'Content-Type': contentType,
|
|
177
|
-
'Content-Length': successResponse.headers.get('Content-Length') || undefined,
|
|
178
|
-
});
|
|
179
|
-
successResponse.body?.pipe(res);
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(data));
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.error('Failed to parse JSON response:', e);
|
|
200
|
+
res.writeHead(502).end('Invalid Upstream Response');
|
|
180
201
|
}
|
|
181
202
|
} else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
return new Promise((resolve, reject) => {
|
|
189
|
-
server.listen(port, () => {
|
|
190
|
-
const address: AddressInfo | string | null = server.address();
|
|
191
|
-
if (!address) {
|
|
192
|
-
console.error('Failed to get server address: address is null');
|
|
193
|
-
reject(new Error('Failed to get server address: address is null'));
|
|
203
|
+
if (!successResponse.body) {
|
|
204
|
+
console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
|
|
205
|
+
res.writeHead(502).end('Empty Response Body');
|
|
194
206
|
return;
|
|
195
207
|
}
|
|
208
|
+
const safeHeaders = {
|
|
209
|
+
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
210
|
+
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
211
|
+
};
|
|
212
|
+
res.writeHead(successResponse.status, safeHeaders);
|
|
213
|
+
successResponse.body.pipe(res);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
let server: HttpServer | HttpsServer;
|
|
218
|
+
if (proxyConfig.https) {
|
|
219
|
+
const { key, cert } = proxyConfig.https;
|
|
220
|
+
try {
|
|
221
|
+
await fsPromises.access(resolvePath(key));
|
|
222
|
+
await fsPromises.access(resolvePath(cert));
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const httpsOptions = {
|
|
228
|
+
key: readFileSync(resolvePath(key)),
|
|
229
|
+
cert: readFileSync(resolvePath(cert)),
|
|
230
|
+
};
|
|
231
|
+
server = createHttpsServer(httpsOptions, requestHandler);
|
|
232
|
+
} else {
|
|
233
|
+
server = createServer(requestHandler);
|
|
234
|
+
}
|
|
196
235
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
238
|
+
if (err.code === 'EADDRINUSE') {
|
|
239
|
+
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
240
|
+
process.exit(1);
|
|
201
241
|
}
|
|
202
|
-
|
|
203
|
-
const addressInfo: AddressInfo = address;
|
|
204
|
-
const actualPort: number = addressInfo.port;
|
|
205
|
-
|
|
206
|
-
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
207
|
-
const portFilePath = join(projectRoot, '.registry-proxy-port');
|
|
208
|
-
console.log(`Proxy server started at http://localhost:${actualPort}`);
|
|
209
|
-
writeFileSync(portFilePath, actualPort.toString(), 'utf8');
|
|
210
|
-
resolve(server);
|
|
242
|
+
reject(err);
|
|
211
243
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
console.log('Server closed.');
|
|
221
|
-
process.exit(0);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
setTimeout(() => {
|
|
225
|
-
console.error('Server did not close in time, forcing exit...');
|
|
226
|
-
process.exit(1);
|
|
227
|
-
}, 5000);
|
|
244
|
+
server.listen(port, () => {
|
|
245
|
+
const address = server.address() as AddressInfo;
|
|
246
|
+
proxyPort = address.port;
|
|
247
|
+
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
248
|
+
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
249
|
+
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
250
|
+
resolve(server);
|
|
228
251
|
});
|
|
229
252
|
});
|
|
230
253
|
}
|
|
231
254
|
|
|
232
255
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
256
|
+
const [,, configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
257
|
+
startProxyServer(
|
|
258
|
+
configPath,
|
|
259
|
+
localYarnPath,
|
|
260
|
+
globalYarnPath,
|
|
261
|
+
parseInt(port, 10) || 0
|
|
262
|
+
).catch(err => {
|
|
263
|
+
console.error('Failed to start server:', err);
|
|
240
264
|
process.exit(1);
|
|
241
265
|
});
|
|
242
266
|
}
|