com.jimuwd.xian.registry-proxy 1.0.4 → 1.0.6
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/README.MD +726 -239
- package/dist/index.js +60 -23
- package/package.json +1 -1
- package/src/index.ts +67 -24
package/README.MD
CHANGED
|
@@ -1,245 +1,732 @@
|
|
|
1
|
+
# Registry Proxy
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
3
|
+
一个轻量级的 Yarn 代理服务器,支持从多个注册表获取包,并支持身份验证。
|
|
4
|
+
|
|
5
|
+
[Switch to English README](#english-version)
|
|
6
|
+
|
|
7
|
+
## 概述
|
|
8
|
+
|
|
9
|
+
本项目提供了一个代理服务器(`registry-proxy`),允许 Yarn 从多个注册表获取包,并支持身份验证令牌。项目还包括一个脚本(`scripts/install-from-proxy-registries.sh`),用于自动化启动代理服务器、安装依赖和清理资源的过程。该设置确保与 Yarn 无缝集成,开发者只需使用标准的 `yarn` 命令即可通过代理安装依赖。
|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- **多注册表支持**:从多个注册表(例如私有和公共注册表)获取包。
|
|
14
|
+
- **身份验证**:支持 `npmAuthToken` 用于认证注册表,可从 `.registry-proxy.yml`、`.yarnrc.yml` 或环境变量中解析令牌。
|
|
15
|
+
- **动态端口分配**:代理服务器使用动态端口(默认 `0`),确保多个项目可以并行运行而不会发生端口冲突。
|
|
16
|
+
- **无缝 Yarn 集成**:运行 `yarn` 时自动启动代理服务器,安装依赖后停止代理。
|
|
17
|
+
- **健壮的错误处理**:使用严格的 Bash 模式(`set -e`、`set -u`、`set -o pipefail`)确保脚本在发生错误时快速失败。
|
|
18
|
+
- **文件放置控制**:确保临时文件(`.registry-proxy-install.lock` 和 `.registry-proxy-port`)始终放置在项目根目录。
|
|
19
|
+
|
|
20
|
+
## 前置条件
|
|
21
|
+
|
|
22
|
+
- **Node.js**:版本 14 或更高。
|
|
23
|
+
- **Yarn**:版本 1.x 或 2.x。
|
|
24
|
+
- **netcat (`nc`)**:用于脚本中的端口可用性检查。安装方法:
|
|
25
|
+
- macOS:`brew install netcat`
|
|
26
|
+
- Ubuntu:`sudo apt-get install netcat`
|
|
27
|
+
- **Bash**:安装脚本需要 Bash 兼容的 shell。
|
|
28
|
+
|
|
29
|
+
## 设置
|
|
30
|
+
|
|
31
|
+
### 1. 安装 `registry-proxy`
|
|
32
|
+
|
|
33
|
+
代理服务器已发布到您的私有注册表。可以将其作为依赖安装,或使用 `npx` 直接运行。
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
yarn add com.jimuwd.xian.registry-proxy --registry https://repo.jimuwd.com/jimuwd/~npm/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
或者,安装脚本使用 `npx` 运行代理服务器,因此无需显式安装。
|
|
40
|
+
|
|
41
|
+
### 2. 配置注册表
|
|
42
|
+
|
|
43
|
+
在项目根目录创建 `.registry-proxy.yml` 文件,定义注册表:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
registries:
|
|
47
|
+
"https://repo.jimuwd.com/jimuwd/~npm/": {}
|
|
48
|
+
"https://registry.npmjs.org/": {}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- 对于需要认证的注册表,可以指定 `npmAuthToken`:
|
|
52
|
+
```yaml
|
|
53
|
+
registries:
|
|
54
|
+
"https://repo.jimuwd.com/jimuwd/~npm/":
|
|
55
|
+
npmAuthToken: "your-token-here"
|
|
56
|
+
"https://registry.npmjs.org/": {}
|
|
57
|
+
```
|
|
58
|
+
- 令牌也可以从 `.yarnrc.yml`(本地或全局)或环境变量中获取。
|
|
59
|
+
|
|
60
|
+
### 3. 配置 Yarn
|
|
61
|
+
|
|
62
|
+
在项目根目录创建 `.yarnrc.yml` 文件,允许 Yarn 使用本地代理:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
unsafeHttpWhitelist:
|
|
66
|
+
- "localhost"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. 创建安装脚本
|
|
70
|
+
|
|
71
|
+
在 `scripts/` 目录下创建 `install-from-proxy-registries.sh` 脚本,用于自动化代理设置和依赖安装:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
#!/bin/bash
|
|
75
|
+
|
|
76
|
+
# 启用严格模式
|
|
77
|
+
set -e # 命令失败时退出
|
|
78
|
+
set -u # 未定义变量时退出
|
|
79
|
+
set -o pipefail # 管道中任一命令失败时退出
|
|
80
|
+
|
|
81
|
+
# 动态确定项目根目录(假设 package.json 所在目录为根目录)
|
|
82
|
+
find_project_root() {
|
|
83
|
+
local dir="$PWD"
|
|
84
|
+
while [ "$dir" != "/" ]; do
|
|
85
|
+
if [ -f "$dir/package.json" ]; then
|
|
86
|
+
echo "$dir"
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
dir=$(dirname "$dir")
|
|
90
|
+
done
|
|
91
|
+
echo "Error: Could not find project root (package.json not found)" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
PROJECT_ROOT=$(find_project_root)
|
|
96
|
+
|
|
97
|
+
# 定义锁文件和端口文件路径(固定在项目根目录)
|
|
98
|
+
LOCK_FILE="$PROJECT_ROOT/.registry-proxy-install.lock"
|
|
99
|
+
PORT_FILE="$PROJECT_ROOT/.registry-proxy-port"
|
|
100
|
+
|
|
101
|
+
# 捕获 Ctrl+C 信号,清理锁文件和端口文件
|
|
102
|
+
cleanup() {
|
|
103
|
+
echo "Caught interrupt signal, cleaning up..."
|
|
104
|
+
rm -f "$LOCK_FILE"
|
|
105
|
+
if [ -n "${PROXY_PID:-}" ]; then
|
|
106
|
+
echo "Stopping proxy server..."
|
|
107
|
+
kill -TERM "$PROXY_PID" 2>/dev/null
|
|
108
|
+
wait "$PROXY_PID" 2>/dev/null || { echo "Error: Failed to stop proxy server"; exit 1; }
|
|
109
|
+
rm -f "$PORT_FILE"
|
|
110
|
+
echo "Proxy server stopped."
|
|
111
|
+
fi
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# 注册信号处理
|
|
116
|
+
trap cleanup SIGINT SIGTERM
|
|
117
|
+
|
|
118
|
+
# 检查是否已经在运行(通过锁文件)
|
|
119
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
120
|
+
echo "Custom install script is already running, skipping to avoid loop."
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# 创建锁文件
|
|
125
|
+
touch "$LOCK_FILE"
|
|
126
|
+
|
|
127
|
+
# 通过 npx 运行 registry-proxy,无需预装依赖
|
|
128
|
+
# 传递项目根目录作为环境变量,供 registry-proxy 使用
|
|
129
|
+
REGISTRY_PROXY_VERSION="${REGISTRY_PROXY_VERSION:-latest}" # 默认使用 latest,可通过环境变量指定
|
|
130
|
+
PROJECT_ROOT="$PROJECT_ROOT" npx com.jimuwd.xian.registry-proxy@"$REGISTRY_PROXY_VERSION" .registry-proxy.yml .yarnrc.yml ~/.yarnrc.yml &
|
|
131
|
+
PROXY_PID=$!
|
|
132
|
+
|
|
133
|
+
# 等待代理服务器启动并写入端口,最多 10 秒
|
|
134
|
+
echo "Waiting for proxy server to start..."
|
|
135
|
+
for i in {1..100}; do
|
|
136
|
+
if [ -f "$PORT_FILE" ]; then
|
|
137
|
+
PROXY_PORT=$(cat "$PORT_FILE")
|
|
138
|
+
if nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
139
|
+
echo "Proxy server is ready on port $PROXY_PORT!"
|
|
140
|
+
break
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
sleep 0.1
|
|
144
|
+
done
|
|
145
|
+
|
|
146
|
+
# 检查是否成功启动
|
|
147
|
+
if [ -z "${PROXY_PORT:-}" ] || ! nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
148
|
+
echo "Error: Proxy server failed to start"
|
|
149
|
+
kill "$PROXY_PID" 2>/dev/null || true # 忽略 kill 失败
|
|
150
|
+
rm -f "$LOCK_FILE"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# 使用动态代理端口运行 yarn install
|
|
155
|
+
# 切换到项目根目录运行 yarn install,确保 yarn 行为一致
|
|
156
|
+
cd "$PROJECT_ROOT"
|
|
157
|
+
yarn install --registry "http://localhost:$PROXY_PORT/"
|
|
158
|
+
|
|
159
|
+
# 停止代理服务器
|
|
160
|
+
echo "Stopping proxy server..."
|
|
161
|
+
kill -TERM "$PROXY_PID"
|
|
162
|
+
wait "$PROXY_PID" 2>/dev/null || { echo "Error: Failed to stop proxy server"; rm -f "$LOCK_FILE"; exit 1; }
|
|
163
|
+
rm -f "$PORT_FILE" # 清理临时文件
|
|
164
|
+
echo "Proxy server stopped."
|
|
165
|
+
|
|
166
|
+
# 清理锁文件
|
|
167
|
+
rm -f "$LOCK_FILE"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 5. 设置脚本权限
|
|
171
|
+
|
|
172
|
+
确保脚本具有可执行权限,并将权限状态提交到版本控制:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
chmod +x scripts/install-from-proxy-registries.sh
|
|
176
|
+
git add scripts/install-from-proxy-registries.sh
|
|
177
|
+
git commit -m "Add install script with executable permission"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 6. 与 Yarn 集成
|
|
181
|
+
|
|
182
|
+
更新 `package.json`,使 `yarn` 命令自动运行脚本:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"scripts": {
|
|
187
|
+
"preinstall": "bash scripts/install-from-proxy-registries.sh",
|
|
188
|
+
"install": "echo 'Custom install script is running via preinstall, skipping default install.'"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- **`preinstall`**:在 Yarn 默认安装过程之前运行脚本。
|
|
194
|
+
- **`install`**:跳过 Yarn 默认安装行为,因为脚本已处理依赖安装。
|
|
195
|
+
|
|
196
|
+
## 使用方法
|
|
197
|
+
|
|
198
|
+
### 安装依赖
|
|
199
|
+
|
|
200
|
+
只需运行标准的 `yarn` 命令即可通过代理安装依赖:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
yarn
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- 脚本将:
|
|
207
|
+
1. 在动态端口上启动代理服务器。
|
|
208
|
+
2. 使用代理安装依赖。
|
|
209
|
+
3. 停止代理服务器并清理临时文件。
|
|
210
|
+
|
|
211
|
+
- 指定 `registry-proxy` 版本:
|
|
212
|
+
```bash
|
|
213
|
+
REGISTRY_PROXY_VERSION=1.0.0 yarn
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 示例输出
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
Waiting for proxy server to start...
|
|
220
|
+
Proxy server started at http://localhost:49152
|
|
221
|
+
Proxy server is ready on port 49152!
|
|
119
222
|
[yarn install 输出]
|
|
120
223
|
Stopping proxy server...
|
|
224
|
+
Received SIGTERM, shutting down...
|
|
225
|
+
Server closed.
|
|
121
226
|
Proxy server stopped.
|
|
227
|
+
Custom install script is running via preinstall, skipping default install.
|
|
122
228
|
```
|
|
123
229
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
- `
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
230
|
+
### 并行构建
|
|
231
|
+
|
|
232
|
+
代理服务器使用动态端口,因此可以并行运行多个项目:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
cd project1 && yarn &
|
|
236
|
+
cd project2 && yarn &
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
每个项目将使用不同的端口(例如 `49152` 和 `49153`)。
|
|
240
|
+
|
|
241
|
+
## 临时文件
|
|
242
|
+
|
|
243
|
+
脚本在执行期间会生成两个临时文件:
|
|
244
|
+
|
|
245
|
+
- **`.registry-proxy-install.lock`**:
|
|
246
|
+
- 用途:防止脚本在同一项目中多次运行(避免 `yarn install` 导致的循环)。
|
|
247
|
+
- 位置:始终放置在项目根目录(`package.json` 所在目录)。
|
|
248
|
+
- **`.registry-proxy-port`**:
|
|
249
|
+
- 用途:存储代理服务器的动态端口号。
|
|
250
|
+
- 位置:始终放置在项目根目录。
|
|
251
|
+
|
|
252
|
+
### 防止文件误放置
|
|
253
|
+
|
|
254
|
+
- 脚本通过查找 `package.json` 动态确定项目根目录,确保临时文件始终放置在正确位置,即使从子目录运行 `yarn`(例如 `cd src && yarn`)。
|
|
255
|
+
- 如果无法找到项目根目录,脚本将报错并退出:
|
|
256
|
+
```
|
|
257
|
+
Error: Could not find project root (package.json not found)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 清理
|
|
261
|
+
|
|
262
|
+
- 临时文件会在以下情况下自动删除:
|
|
263
|
+
- 脚本成功完成时。
|
|
264
|
+
- 中断(例如 `Ctrl+C`)时。
|
|
265
|
+
- 脚本使用信号处理(`trap`)确保即使中断也能执行清理。
|
|
266
|
+
|
|
267
|
+
## 注意事项
|
|
268
|
+
|
|
269
|
+
1. **严格模式**:
|
|
270
|
+
- 脚本使用 Bash 严格模式(`set -e`、`set -u`、`set -o pipefail`)确保健壮的错误处理:
|
|
271
|
+
- `set -e`:任何命令失败时退出。
|
|
272
|
+
- `set -u`:未定义变量时退出。
|
|
273
|
+
- `set -o pipefail`:管道中任一命令失败时退出。
|
|
274
|
+
|
|
275
|
+
2. **并行执行**:
|
|
276
|
+
- 锁文件(`.registry-proxy-install.lock`)仅在同一项目内防止重复执行,不影响不同项目的并行构建。
|
|
277
|
+
- 如果需要在同一项目中运行多个 `yarn` 进程(例如 CI 环境),可以考虑为每个进程使用唯一的锁文件(例如基于进程 ID)。
|
|
278
|
+
|
|
279
|
+
3. **依赖**:
|
|
280
|
+
- 确保已安装 `nc`(netcat)以进行端口可用性检查。
|
|
281
|
+
- 脚本使用 `npx` 运行 `registry-proxy`,无需预安装。
|
|
282
|
+
|
|
283
|
+
## 故障排除
|
|
284
|
+
|
|
285
|
+
### 代理服务器无法启动
|
|
286
|
+
|
|
287
|
+
- **症状**:`Error: Proxy server failed to start`
|
|
288
|
+
- **可能原因**:
|
|
289
|
+
- 指定的 `REGISTRY_PROXY_VERSION` 无效。
|
|
290
|
+
- 网络问题导致 `npx` 无法下载包。
|
|
291
|
+
- **解决方法**:
|
|
292
|
+
- 验证版本:`REGISTRY_PROXY_VERSION=latest yarn`。
|
|
293
|
+
- 检查网络连接并重试。
|
|
294
|
+
|
|
295
|
+
### 临时文件未清理
|
|
296
|
+
|
|
297
|
+
- **症状**:`.registry-proxy-install.lock` 或 `.registry-proxy-port` 在脚本执行后仍存在。
|
|
298
|
+
- **可能原因**:脚本被异常终止(例如 `kill -9` 而非 `Ctrl+C`)。
|
|
299
|
+
- **解决方法**:
|
|
300
|
+
- 手动删除文件:
|
|
301
|
+
```bash
|
|
302
|
+
rm -f .registry-proxy-install.lock .registry-proxy-port
|
|
303
|
+
```
|
|
304
|
+
- 使用 `Ctrl+C` 或 `kill -TERM` 确保正常清理。
|
|
305
|
+
|
|
306
|
+
### Yarn 安装失败
|
|
307
|
+
|
|
308
|
+
- **症状**:`yarn install` 失败并显示网络错误。
|
|
309
|
+
- **可能原因**:代理服务器未正确启动,或注册表不可达。
|
|
310
|
+
- **解决方法**:
|
|
311
|
+
- 检查代理服务器日志(由 `registry-proxy` 输出)。
|
|
312
|
+
- 验证 `.registry-proxy.yml` 中的注册表 URL。
|
|
313
|
+
|
|
314
|
+
## 测试
|
|
315
|
+
|
|
316
|
+
### 正常安装
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
cd project1
|
|
320
|
+
yarn
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 子目录安装
|
|
324
|
+
|
|
325
|
+
从子目录运行 `yarn`,验证临时文件是否放置在项目根目录:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
cd project1/src
|
|
329
|
+
yarn
|
|
330
|
+
ls ../.registry-proxy-install.lock ../.registry-proxy-port
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 中断处理
|
|
334
|
+
|
|
335
|
+
运行 `yarn` 并在执行期间按 `Ctrl+C`,验证清理:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
cd project1
|
|
339
|
+
yarn
|
|
340
|
+
# 在 "Waiting for proxy server to start..." 后按 Ctrl+C
|
|
341
|
+
ls .registry-proxy-install.lock .registry-proxy-port
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
预期输出:文件应不存在。
|
|
345
|
+
|
|
346
|
+
### 并行构建
|
|
347
|
+
|
|
348
|
+
并行运行多个项目,验证端口分配:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
cd project1 && yarn &
|
|
352
|
+
cd project2 && yarn &
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## 贡献
|
|
356
|
+
|
|
357
|
+
1. Fork 本仓库。
|
|
358
|
+
2. 创建功能分支:`git checkout -b feature-name`。
|
|
359
|
+
3. 提交更改:`git commit -m "Add feature"`。
|
|
360
|
+
4. 推送分支:`git push origin feature-name`。
|
|
361
|
+
5. 提交 Pull Request。
|
|
362
|
+
|
|
363
|
+
## 许可证
|
|
364
|
+
|
|
365
|
+
MIT 许可证。详见 [LICENSE](LICENSE)。
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# English Version
|
|
369
|
+
|
|
370
|
+
## Registry Proxy
|
|
371
|
+
|
|
372
|
+
A lightweight proxy server for Yarn to fetch packages from multiple registries with authentication support.
|
|
373
|
+
|
|
374
|
+
## Overview
|
|
375
|
+
|
|
376
|
+
This project provides a proxy server (`registry-proxy`) that allows Yarn to fetch packages from multiple registries, with support for authentication tokens. It also includes a script (`scripts/install-from-proxy-registries.sh`) to automate the process of starting the proxy server, installing dependencies, and cleaning up resources. The setup ensures seamless integration with Yarn, allowing developers to use the standard `yarn` command to install dependencies via the proxy.
|
|
377
|
+
|
|
378
|
+
## Features
|
|
379
|
+
|
|
380
|
+
- **Multi-Registry Support**: Fetch packages from multiple registries (e.g., private and public registries).
|
|
381
|
+
- **Authentication**: Supports `npmAuthToken` for authenticated registries, with token resolution from `.registry-proxy.yml`, `.yarnrc.yml`, or environment variables.
|
|
382
|
+
- **Dynamic Port Allocation**: The proxy server uses a dynamic port (default `0`), ensuring multiple projects can run in parallel without port conflicts.
|
|
383
|
+
- **Seamless Yarn Integration**: Automatically starts the proxy server when running `yarn`, installs dependencies, and stops the proxy afterward.
|
|
384
|
+
- **Robust Error Handling**: Uses strict Bash modes (`set -e`, `set -u`, `set -o pipefail`) to ensure the script fails fast on errors.
|
|
385
|
+
- **File Placement Control**: Ensures temporary files (`.registry-proxy-install.lock` and `.registry-proxy-port`) are always placed in the project root directory.
|
|
386
|
+
|
|
387
|
+
## Prerequisites
|
|
388
|
+
|
|
389
|
+
- **Node.js**: Version 14 or higher.
|
|
390
|
+
- **Yarn**: Version 1.x or 2.x.
|
|
391
|
+
- **netcat (`nc`)**: Required for port availability checks in the install script. Install via:
|
|
392
|
+
- On macOS: `brew install netcat`
|
|
393
|
+
- On Ubuntu: `sudo apt-get install netcat`
|
|
394
|
+
- **Bash**: The install script requires a Bash-compatible shell.
|
|
395
|
+
|
|
396
|
+
## Setup
|
|
397
|
+
|
|
398
|
+
### 1. Install `registry-proxy`
|
|
399
|
+
|
|
400
|
+
The proxy server is published to your private registry. Install it as a dependency or use `npx` to run it directly.
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
yarn add com.jimuwd.xian.registry-proxy --registry https://repo.jimuwd.com/jimuwd/~npm/
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Alternatively, the install script uses `npx` to run the proxy server, so you don't need to install it explicitly.
|
|
407
|
+
|
|
408
|
+
### 2. Configure Registries
|
|
409
|
+
|
|
410
|
+
Create a `.registry-proxy.yml` file in your project root to define the registries:
|
|
411
|
+
|
|
412
|
+
```yaml
|
|
413
|
+
registries:
|
|
414
|
+
"https://repo.jimuwd.com/jimuwd/~npm/": {}
|
|
415
|
+
"https://registry.npmjs.org/": {}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- You can specify `npmAuthToken` for authenticated registries:
|
|
419
|
+
```yaml
|
|
420
|
+
registries:
|
|
421
|
+
"https://repo.jimuwd.com/jimuwd/~npm/":
|
|
422
|
+
npmAuthToken: "your-token-here"
|
|
423
|
+
"https://registry.npmjs.org/": {}
|
|
424
|
+
```
|
|
425
|
+
- Tokens can also be sourced from `.yarnrc.yml` (local or global) or environment variables.
|
|
426
|
+
|
|
427
|
+
### 3. Configure Yarn
|
|
428
|
+
|
|
429
|
+
Create a `.yarnrc.yml` file in your project root to allow Yarn to use the local proxy:
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
unsafeHttpWhitelist:
|
|
433
|
+
- "localhost"
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 4. Create the Install Script
|
|
437
|
+
|
|
438
|
+
Create a script at `scripts/install-from-proxy-registries.sh` to automate the proxy setup and dependency installation:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
#!/bin/bash
|
|
442
|
+
|
|
443
|
+
# 启用严格模式
|
|
444
|
+
set -e # 命令失败时退出
|
|
445
|
+
set -u # 未定义变量时退出
|
|
446
|
+
set -o pipefail # 管道中任一命令失败时退出
|
|
447
|
+
|
|
448
|
+
# 动态确定项目根目录(假设 package.json 所在目录为根目录)
|
|
449
|
+
find_project_root() {
|
|
450
|
+
local dir="$PWD"
|
|
451
|
+
while [ "$dir" != "/" ]; do
|
|
452
|
+
if [ -f "$dir/package.json" ]; then
|
|
453
|
+
echo "$dir"
|
|
454
|
+
return 0
|
|
455
|
+
fi
|
|
456
|
+
dir=$(dirname "$dir")
|
|
457
|
+
done
|
|
458
|
+
echo "Error: Could not find project root (package.json not found)" >&2
|
|
459
|
+
exit 1
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
PROJECT_ROOT=$(find_project_root)
|
|
463
|
+
|
|
464
|
+
# 定义锁文件和端口文件路径(固定在项目根目录)
|
|
465
|
+
LOCK_FILE="$PROJECT_ROOT/.registry-proxy-install.lock"
|
|
466
|
+
PORT_FILE="$PROJECT_ROOT/.registry-proxy-port"
|
|
467
|
+
|
|
468
|
+
# 捕获 Ctrl+C 信号,清理锁文件和端口文件
|
|
469
|
+
cleanup() {
|
|
470
|
+
echo "Caught interrupt signal, cleaning up..."
|
|
471
|
+
rm -f "$LOCK_FILE"
|
|
472
|
+
if [ -n "${PROXY_PID:-}" ]; then
|
|
473
|
+
echo "Stopping proxy server..."
|
|
474
|
+
kill -TERM "$PROXY_PID" 2>/dev/null
|
|
475
|
+
wait "$PROXY_PID" 2>/dev/null || { echo "Error: Failed to stop proxy server"; exit 1; }
|
|
476
|
+
rm -f "$PORT_FILE"
|
|
477
|
+
echo "Proxy server stopped."
|
|
478
|
+
fi
|
|
479
|
+
exit 1
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# 注册信号处理
|
|
483
|
+
trap cleanup SIGINT SIGTERM
|
|
484
|
+
|
|
485
|
+
# 检查是否已经在运行(通过锁文件)
|
|
486
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
487
|
+
echo "Custom install script is already running, skipping to avoid loop."
|
|
488
|
+
exit 0
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
# 创建锁文件
|
|
492
|
+
touch "$LOCK_FILE"
|
|
493
|
+
|
|
494
|
+
# 通过 npx 运行 registry-proxy,无需预装依赖
|
|
495
|
+
# 传递项目根目录作为环境变量,供 registry-proxy 使用
|
|
496
|
+
REGISTRY_PROXY_VERSION="${REGISTRY_PROXY_VERSION:-latest}" # 默认使用 latest,可通过环境变量指定
|
|
497
|
+
PROJECT_ROOT="$PROJECT_ROOT" npx com.jimuwd.xian.registry-proxy@"$REGISTRY_PROXY_VERSION" .registry-proxy.yml .yarnrc.yml ~/.yarnrc.yml &
|
|
498
|
+
PROXY_PID=$!
|
|
499
|
+
|
|
500
|
+
# 等待代理服务器启动并写入端口,最多 10 秒
|
|
501
|
+
echo "Waiting for proxy server to start..."
|
|
502
|
+
for i in {1..100}; do
|
|
503
|
+
if [ -f "$PORT_FILE" ]; then
|
|
504
|
+
PROXY_PORT=$(cat "$PORT_FILE")
|
|
505
|
+
if nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
506
|
+
echo "Proxy server is ready on port $PROXY_PORT!"
|
|
507
|
+
break
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
510
|
+
sleep 0.1
|
|
511
|
+
done
|
|
512
|
+
|
|
513
|
+
# 检查是否成功启动
|
|
514
|
+
if [ -z "${PROXY_PORT:-}" ] || ! nc -z localhost "$PROXY_PORT" 2>/dev/null; then
|
|
515
|
+
echo "Error: Proxy server failed to start"
|
|
516
|
+
kill "$PROXY_PID" 2>/dev/null || true # 忽略 kill 失败
|
|
517
|
+
rm -f "$LOCK_FILE"
|
|
518
|
+
exit 1
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# 使用动态代理端口运行 yarn install
|
|
522
|
+
# 切换到项目根目录运行 yarn install,确保 yarn 行为一致
|
|
523
|
+
cd "$PROJECT_ROOT"
|
|
524
|
+
yarn install --registry "http://localhost:$PROXY_PORT/"
|
|
525
|
+
|
|
526
|
+
# 停止代理服务器
|
|
527
|
+
echo "Stopping proxy server..."
|
|
528
|
+
kill -TERM "$PROXY_PID"
|
|
529
|
+
wait "$PROXY_PID" 2>/dev/null || { echo "Error: Failed to stop proxy server"; rm -f "$LOCK_FILE"; exit 1; }
|
|
530
|
+
rm -f "$PORT_FILE" # 清理临时文件
|
|
531
|
+
echo "Proxy server stopped."
|
|
532
|
+
|
|
533
|
+
# 清理锁文件
|
|
534
|
+
rm -f "$LOCK_FILE"
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### 5. Set Script Permissions
|
|
538
|
+
|
|
539
|
+
Ensure the script is executable and commit the permission to version control:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
chmod +x scripts/install-from-proxy-registries.sh
|
|
543
|
+
git add scripts/install-from-proxy-registries.sh
|
|
544
|
+
git commit -m "Add install script with executable permission"
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### 6. Integrate with Yarn
|
|
548
|
+
|
|
549
|
+
Update your `package.json` to run the script automatically when `yarn` is executed:
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
{
|
|
553
|
+
"scripts": {
|
|
554
|
+
"preinstall": "bash scripts/install-from-proxy-registries.sh",
|
|
555
|
+
"install": "echo 'Custom install script is running via preinstall, skipping default install.'"
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- **`preinstall`**: Runs the script before Yarn's default install process.
|
|
561
|
+
- **`install`**: Skips Yarn's default install behavior, as the script already handles dependency installation.
|
|
562
|
+
|
|
563
|
+
## Usage
|
|
564
|
+
|
|
565
|
+
### Install Dependencies
|
|
566
|
+
|
|
567
|
+
Simply run the standard `yarn` command to install dependencies via the proxy:
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
yarn
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
- The script will:
|
|
574
|
+
1. Start the proxy server on a dynamic port.
|
|
575
|
+
2. Install dependencies using the proxy.
|
|
576
|
+
3. Stop the proxy server and clean up temporary files.
|
|
577
|
+
|
|
578
|
+
- To specify a version of `registry-proxy`:
|
|
579
|
+
```bash
|
|
580
|
+
REGISTRY_PROXY_VERSION=1.0.0 yarn
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Example Output
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
Waiting for proxy server to start...
|
|
587
|
+
Proxy server started at http://localhost:49152
|
|
588
|
+
Proxy server is ready on port 49152!
|
|
589
|
+
[yarn install output]
|
|
590
|
+
Stopping proxy server...
|
|
591
|
+
Received SIGTERM, shutting down...
|
|
592
|
+
Server closed.
|
|
593
|
+
Proxy server stopped.
|
|
594
|
+
Custom install script is running via preinstall, skipping default install.
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Parallel Builds
|
|
598
|
+
|
|
599
|
+
The proxy server uses dynamic ports, so you can run multiple projects in parallel:
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
cd project1 && yarn &
|
|
603
|
+
cd project2 && yarn &
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Each project will use a different port (e.g., `49152` and `49153`).
|
|
607
|
+
|
|
608
|
+
## Temporary Files
|
|
609
|
+
|
|
610
|
+
The script generates two temporary files during execution:
|
|
611
|
+
|
|
612
|
+
- **`.registry-proxy-install.lock`**:
|
|
613
|
+
- Purpose: Prevents the script from running multiple times in the same project (avoids loops caused by `yarn install`).
|
|
614
|
+
- Location: Always placed in the project root directory (where `package.json` resides).
|
|
615
|
+
- **`.registry-proxy-port`**:
|
|
616
|
+
- Purpose: Stores the dynamic port number of the proxy server.
|
|
617
|
+
- Location: Always placed in the project root directory.
|
|
618
|
+
|
|
619
|
+
### Preventing Misplacement
|
|
620
|
+
|
|
621
|
+
- The script dynamically determines the project root by locating `package.json`, ensuring temporary files are always placed in the correct location, even if `yarn` is run from a subdirectory (e.g., `cd src && yarn`).
|
|
622
|
+
- If the project root cannot be found, the script will exit with an error:
|
|
623
|
+
```
|
|
624
|
+
Error: Could not find project root (package.json not found)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Cleanup
|
|
628
|
+
|
|
629
|
+
- Temporary files are automatically removed:
|
|
630
|
+
- On successful completion of the script.
|
|
631
|
+
- On interruption (e.g., `Ctrl+C`).
|
|
632
|
+
- The script uses signal handling (`trap`) to ensure cleanup occurs even if interrupted.
|
|
633
|
+
|
|
634
|
+
## Notes
|
|
635
|
+
|
|
636
|
+
1. **Strict Mode**:
|
|
637
|
+
- The script uses Bash strict modes (`set -e`, `set -u`, `set -o pipefail`) to ensure robust error handling:
|
|
638
|
+
- `set -e`: Exits on any command failure.
|
|
639
|
+
- `set -u`: Exits on undefined variables.
|
|
640
|
+
- `set -o pipefail`: Exits if any command in a pipeline fails.
|
|
641
|
+
|
|
642
|
+
2. **Parallel Execution**:
|
|
643
|
+
- The lock file (`.registry-proxy-install.lock`) prevents multiple executions within the same project but does not affect different projects.
|
|
644
|
+
- If you need to run multiple `yarn` processes in the same project (e.g., in a CI environment), consider using a unique lock file per process (e.g., based on process ID).
|
|
645
|
+
|
|
646
|
+
3. **Dependencies**:
|
|
647
|
+
- Ensure `nc` (netcat) is installed for port availability checks.
|
|
648
|
+
- The script uses `npx` to run `registry-proxy`, so no pre-installation is required.
|
|
649
|
+
|
|
650
|
+
## Troubleshooting
|
|
651
|
+
|
|
652
|
+
### Proxy Server Fails to Start
|
|
653
|
+
|
|
654
|
+
- **Symptom**: `Error: Proxy server failed to start`
|
|
655
|
+
- **Possible Causes**:
|
|
656
|
+
- The specified `REGISTRY_PROXY_VERSION` is invalid.
|
|
657
|
+
- Network issues prevent `npx` from downloading the package.
|
|
658
|
+
- **Solution**:
|
|
659
|
+
- Verify the version: `REGISTRY_PROXY_VERSION=latest yarn`.
|
|
660
|
+
- Check network connectivity and retry.
|
|
661
|
+
|
|
662
|
+
### Temporary Files Not Cleaned Up
|
|
663
|
+
|
|
664
|
+
- **Symptom**: `.registry-proxy-install.lock` or `.registry-proxy-port` remains after script execution.
|
|
665
|
+
- **Possible Cause**: The script was terminated abnormally (e.g., `kill -9` instead of `Ctrl+C`).
|
|
666
|
+
- **Solution**:
|
|
667
|
+
- Manually remove the files:
|
|
668
|
+
```bash
|
|
669
|
+
rm -f .registry-proxy-install.lock .registry-proxy-port
|
|
670
|
+
```
|
|
671
|
+
- Use `Ctrl+C` or `kill -TERM` to ensure proper cleanup.
|
|
672
|
+
|
|
673
|
+
### Yarn Install Fails
|
|
674
|
+
|
|
675
|
+
- **Symptom**: `yarn install` fails with a network error.
|
|
676
|
+
- **Possible Cause**: The proxy server did not start correctly, or the registry is unreachable.
|
|
677
|
+
- **Solution**:
|
|
678
|
+
- Check the proxy server logs (output by `registry-proxy`).
|
|
679
|
+
- Verify the registry URLs in `.registry-proxy.yml`.
|
|
680
|
+
|
|
681
|
+
## Testing
|
|
682
|
+
|
|
683
|
+
### Normal Installation
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
cd project1
|
|
687
|
+
yarn
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Subdirectory Installation
|
|
691
|
+
|
|
692
|
+
Run `yarn` from a subdirectory to verify that temporary files are placed in the project root:
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
cd project1/src
|
|
696
|
+
yarn
|
|
697
|
+
ls ../.registry-proxy-install.lock ../.registry-proxy-port
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Interrupt Handling
|
|
701
|
+
|
|
702
|
+
Run `yarn` and press `Ctrl+C` during execution to verify cleanup:
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
cd project1
|
|
706
|
+
yarn
|
|
707
|
+
# Press Ctrl+C after "Waiting for proxy server to start..."
|
|
708
|
+
ls .registry-proxy-install.lock .registry-proxy-port
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Expected output: Files should not exist.
|
|
712
|
+
|
|
713
|
+
### Parallel Builds
|
|
714
|
+
|
|
715
|
+
Run multiple projects in parallel to verify port allocation:
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
cd project1 && yarn &
|
|
719
|
+
cd project2 && yarn &
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
## Contributing
|
|
723
|
+
|
|
724
|
+
1. Fork the repository.
|
|
725
|
+
2. Create a feature branch: `git checkout -b feature-name`.
|
|
726
|
+
3. Commit your changes: `git commit -m "Add feature"`.
|
|
727
|
+
4. Push to the branch: `git push origin feature-name`.
|
|
728
|
+
5. Open a pull request.
|
|
729
|
+
|
|
730
|
+
## License
|
|
731
|
+
|
|
732
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
package/dist/index.js
CHANGED
|
@@ -4,49 +4,54 @@ import { readFile } from 'fs/promises';
|
|
|
4
4
|
import { load } from 'js-yaml';
|
|
5
5
|
import fetch from 'node-fetch';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
|
|
7
|
+
import { join, resolve } from 'path';
|
|
8
|
+
import { writeFileSync } from 'fs';
|
|
9
9
|
function normalizeUrl(url) {
|
|
10
10
|
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
11
11
|
}
|
|
12
|
+
function resolvePath(path) {
|
|
13
|
+
return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
14
|
+
}
|
|
12
15
|
async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYarnConfigPath = './.yarnrc.yml', globalYarnConfigPath = join(homedir(), '.yarnrc.yml')) {
|
|
16
|
+
const resolvedProxyPath = resolvePath(proxyConfigPath);
|
|
17
|
+
const resolvedLocalYarnPath = resolvePath(localYarnConfigPath);
|
|
18
|
+
const resolvedGlobalYarnPath = resolvePath(globalYarnConfigPath);
|
|
13
19
|
let proxyConfig = { registries: {} };
|
|
14
20
|
try {
|
|
15
|
-
const proxyYamlContent = await readFile(
|
|
21
|
+
const proxyYamlContent = await readFile(resolvedProxyPath, 'utf8');
|
|
16
22
|
proxyConfig = load(proxyYamlContent);
|
|
17
|
-
console.log(`Loaded proxy config from ${
|
|
23
|
+
console.log(`Loaded proxy config from ${resolvedProxyPath}`);
|
|
18
24
|
}
|
|
19
25
|
catch (e) {
|
|
20
|
-
console.error(`Failed to load ${
|
|
26
|
+
console.error(`Failed to load ${resolvedProxyPath}: ${e.message}`);
|
|
21
27
|
process.exit(1);
|
|
22
28
|
}
|
|
23
29
|
if (!proxyConfig.registries || !Object.keys(proxyConfig.registries).length) {
|
|
24
|
-
console.error(`No registries found in ${
|
|
30
|
+
console.error(`No registries found in ${resolvedProxyPath}`);
|
|
25
31
|
process.exit(1);
|
|
26
32
|
}
|
|
27
33
|
let localYarnConfig = { npmRegistries: {} };
|
|
28
34
|
try {
|
|
29
|
-
const localYamlContent = await readFile(
|
|
35
|
+
const localYamlContent = await readFile(resolvedLocalYarnPath, 'utf8');
|
|
30
36
|
localYarnConfig = load(localYamlContent);
|
|
31
|
-
console.log(`Loaded local Yarn config from ${
|
|
37
|
+
console.log(`Loaded local Yarn config from ${resolvedLocalYarnPath}`);
|
|
32
38
|
}
|
|
33
39
|
catch (e) {
|
|
34
|
-
console.warn(`Failed to load ${
|
|
40
|
+
console.warn(`Failed to load ${resolvedLocalYarnPath}: ${e.message}`);
|
|
35
41
|
}
|
|
36
42
|
let globalYarnConfig = { npmRegistries: {} };
|
|
37
43
|
try {
|
|
38
|
-
const globalYamlContent = await readFile(
|
|
44
|
+
const globalYamlContent = await readFile(resolvedGlobalYarnPath, 'utf8');
|
|
39
45
|
globalYarnConfig = load(globalYamlContent);
|
|
40
|
-
console.log(`Loaded global Yarn config from ${
|
|
46
|
+
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
41
47
|
}
|
|
42
48
|
catch (e) {
|
|
43
|
-
console.warn(`Failed to load ${
|
|
49
|
+
console.warn(`Failed to load ${resolvedGlobalYarnPath}: ${e.message}`);
|
|
44
50
|
}
|
|
45
|
-
// 使用 Map 合并重复的 URL
|
|
46
51
|
const registryMap = new Map();
|
|
47
52
|
for (const [url, regConfig] of Object.entries(proxyConfig.registries)) {
|
|
48
53
|
const normalizedUrl = normalizeUrl(url);
|
|
49
|
-
registryMap.set(normalizedUrl, regConfig);
|
|
54
|
+
registryMap.set(normalizedUrl, regConfig);
|
|
50
55
|
}
|
|
51
56
|
const registries = Array.from(registryMap.entries()).map(([url, regConfig]) => {
|
|
52
57
|
let token;
|
|
@@ -56,7 +61,7 @@ async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYa
|
|
|
56
61
|
const normalizedUrl = normalizeUrl(url);
|
|
57
62
|
if (!token && localYarnConfig.npmRegistries?.[normalizedUrl] && 'npmAuthToken' in localYarnConfig.npmRegistries[normalizedUrl]) {
|
|
58
63
|
token = localYarnConfig.npmRegistries[normalizedUrl].npmAuthToken?.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || localYarnConfig.npmRegistries[normalizedUrl].npmAuthToken;
|
|
59
|
-
console.log(`Token for ${url} not found in ${
|
|
64
|
+
console.log(`Token for ${url} not found in ${resolvedProxyPath}, using local Yarn config`);
|
|
60
65
|
}
|
|
61
66
|
if (!token && globalYarnConfig.npmRegistries?.[normalizedUrl] && 'npmAuthToken' in globalYarnConfig.npmRegistries[normalizedUrl]) {
|
|
62
67
|
token = globalYarnConfig.npmRegistries[normalizedUrl].npmAuthToken?.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || globalYarnConfig.npmRegistries[normalizedUrl].npmAuthToken;
|
|
@@ -67,7 +72,7 @@ async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYa
|
|
|
67
72
|
});
|
|
68
73
|
return registries;
|
|
69
74
|
}
|
|
70
|
-
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port =
|
|
75
|
+
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
71
76
|
console.log('Starting proxy server...');
|
|
72
77
|
const registries = await loadRegistries(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
73
78
|
const server = createServer(async (req, res) => {
|
|
@@ -105,19 +110,51 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
105
110
|
res.end('Package not found');
|
|
106
111
|
}
|
|
107
112
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
server.listen(port, () => {
|
|
115
|
+
const address = server.address();
|
|
116
|
+
if (!address) {
|
|
117
|
+
console.error('Failed to get server address: address is null');
|
|
118
|
+
reject(new Error('Failed to get server address: address is null'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (typeof address === 'string') {
|
|
122
|
+
console.error('Server bound to a path (e.g., Unix socket), which is not supported');
|
|
123
|
+
reject(new Error('Server bound to a path, expected a TCP port'));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const addressInfo = address;
|
|
127
|
+
const actualPort = addressInfo.port;
|
|
128
|
+
// 从环境变量获取项目根目录,写入端口文件
|
|
129
|
+
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
130
|
+
const portFilePath = join(projectRoot, '.registry-proxy-port');
|
|
131
|
+
console.log(`Proxy server started at http://localhost:${actualPort}`);
|
|
132
|
+
writeFileSync(portFilePath, actualPort.toString(), 'utf8');
|
|
133
|
+
resolve(server);
|
|
134
|
+
});
|
|
135
|
+
process.on('SIGTERM', () => {
|
|
136
|
+
console.log('Received SIGTERM, shutting down...');
|
|
137
|
+
server.close((err) => {
|
|
138
|
+
if (err) {
|
|
139
|
+
console.error('Error closing server:', err.message);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
console.log('Server closed.');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
});
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
console.error('Server did not close in time, forcing exit...');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}, 5000);
|
|
149
|
+
});
|
|
112
150
|
});
|
|
113
|
-
return server;
|
|
114
151
|
}
|
|
115
152
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
116
153
|
const proxyConfigPath = process.argv[2];
|
|
117
154
|
const localYarnConfigPath = process.argv[3];
|
|
118
155
|
const globalYarnConfigPath = process.argv[4];
|
|
119
|
-
const port = parseInt(process.argv[5], 10) ||
|
|
120
|
-
console.log(`CLI: proxyConfigPath=${proxyConfigPath || './.registry-proxy.yml'}, localYarnConfigPath=${localYarnConfigPath || './.yarnrc.yml'}, globalYarnConfigPath=${globalYarnConfigPath || join(homedir(), '.yarnrc.yml')}, port=${port}`);
|
|
156
|
+
const port = parseInt(process.argv[5], 10) || 0;
|
|
157
|
+
console.log(`CLI: proxyConfigPath=${proxyConfigPath || './.registry-proxy.yml'}, localYarnConfigPath=${localYarnConfigPath || './.yarnrc.yml'}, globalYarnConfigPath=${globalYarnConfigPath || join(homedir(), '.yarnrc.yml')}, port=${port || 'dynamic'}`);
|
|
121
158
|
startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port).catch(err => {
|
|
122
159
|
console.error('Startup failed:', err.message);
|
|
123
160
|
process.exit(1);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,59 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer, Server } from 'http';
|
|
3
|
+
import { AddressInfo } from 'net'; // 导入 AddressInfo 类型
|
|
3
4
|
import { readFile } from 'fs/promises';
|
|
4
5
|
import { load } from 'js-yaml';
|
|
5
6
|
import fetch, { Response } from 'node-fetch';
|
|
6
7
|
import { homedir } from 'os';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
import { writeFileSync } from 'fs';
|
|
8
10
|
|
|
9
11
|
interface RegistryConfig { npmAuthToken?: string; }
|
|
10
12
|
interface ProxyConfig { registries: Record<string, RegistryConfig | null>; }
|
|
11
13
|
interface YarnConfig { npmRegistries?: Record<string, RegistryConfig | null>; }
|
|
12
14
|
|
|
13
|
-
// 规范化 URL,去除尾部斜杠
|
|
14
15
|
function normalizeUrl(url: string): string {
|
|
15
16
|
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function resolvePath(path: string): string {
|
|
20
|
+
return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYarnConfigPath = './.yarnrc.yml', globalYarnConfigPath = join(homedir(), '.yarnrc.yml')): Promise<{ url: string; token?: string }[]> {
|
|
24
|
+
const resolvedProxyPath = resolvePath(proxyConfigPath);
|
|
25
|
+
const resolvedLocalYarnPath = resolvePath(localYarnConfigPath);
|
|
26
|
+
const resolvedGlobalYarnPath = resolvePath(globalYarnConfigPath);
|
|
27
|
+
|
|
19
28
|
let proxyConfig: ProxyConfig = { registries: {} };
|
|
20
29
|
try {
|
|
21
|
-
const proxyYamlContent = await readFile(
|
|
30
|
+
const proxyYamlContent = await readFile(resolvedProxyPath, 'utf8');
|
|
22
31
|
proxyConfig = load(proxyYamlContent) as ProxyConfig;
|
|
23
|
-
console.log(`Loaded proxy config from ${
|
|
32
|
+
console.log(`Loaded proxy config from ${resolvedProxyPath}`);
|
|
24
33
|
} catch (e) {
|
|
25
|
-
console.error(`Failed to load ${
|
|
34
|
+
console.error(`Failed to load ${resolvedProxyPath}: ${(e as Error).message}`);
|
|
26
35
|
process.exit(1);
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
if (!proxyConfig.registries || !Object.keys(proxyConfig.registries).length) {
|
|
30
|
-
console.error(`No registries found in ${
|
|
39
|
+
console.error(`No registries found in ${resolvedProxyPath}`);
|
|
31
40
|
process.exit(1);
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
let localYarnConfig: YarnConfig = { npmRegistries: {} };
|
|
35
44
|
try {
|
|
36
|
-
const localYamlContent = await readFile(
|
|
45
|
+
const localYamlContent = await readFile(resolvedLocalYarnPath, 'utf8');
|
|
37
46
|
localYarnConfig = load(localYamlContent) as YarnConfig;
|
|
38
|
-
console.log(`Loaded local Yarn config from ${
|
|
47
|
+
console.log(`Loaded local Yarn config from ${resolvedLocalYarnPath}`);
|
|
39
48
|
} catch (e) {
|
|
40
|
-
console.warn(`Failed to load ${
|
|
49
|
+
console.warn(`Failed to load ${resolvedLocalYarnPath}: ${(e as Error).message}`);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
let globalYarnConfig: YarnConfig = { npmRegistries: {} };
|
|
44
53
|
try {
|
|
45
|
-
const globalYamlContent = await readFile(
|
|
54
|
+
const globalYamlContent = await readFile(resolvedGlobalYarnPath, 'utf8');
|
|
46
55
|
globalYarnConfig = load(globalYamlContent) as YarnConfig;
|
|
47
|
-
console.log(`Loaded global Yarn config from ${
|
|
56
|
+
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
48
57
|
} catch (e) {
|
|
49
|
-
console.warn(`Failed to load ${
|
|
58
|
+
console.warn(`Failed to load ${resolvedGlobalYarnPath}: ${(e as Error).message}`);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
// 使用 Map 合并重复的 URL
|
|
53
61
|
const registryMap = new Map<string, RegistryConfig | null>();
|
|
54
62
|
for (const [url, regConfig] of Object.entries(proxyConfig.registries)) {
|
|
55
63
|
const normalizedUrl = normalizeUrl(url);
|
|
56
|
-
registryMap.set(normalizedUrl, regConfig);
|
|
64
|
+
registryMap.set(normalizedUrl, regConfig);
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
const registries = Array.from(registryMap.entries()).map(([url, regConfig]) => {
|
|
@@ -66,7 +74,7 @@ async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYa
|
|
|
66
74
|
const normalizedUrl = normalizeUrl(url);
|
|
67
75
|
if (!token && localYarnConfig.npmRegistries?.[normalizedUrl] && 'npmAuthToken' in localYarnConfig.npmRegistries[normalizedUrl]) {
|
|
68
76
|
token = localYarnConfig.npmRegistries[normalizedUrl]!.npmAuthToken?.replace(/\${(.+)}/, (_, key) => process.env[key] || '') || localYarnConfig.npmRegistries[normalizedUrl]!.npmAuthToken;
|
|
69
|
-
console.log(`Token for ${url} not found in ${
|
|
77
|
+
console.log(`Token for ${url} not found in ${resolvedProxyPath}, using local Yarn config`);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
if (!token && globalYarnConfig.npmRegistries?.[normalizedUrl] && 'npmAuthToken' in globalYarnConfig.npmRegistries[normalizedUrl]) {
|
|
@@ -81,7 +89,7 @@ async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYa
|
|
|
81
89
|
return registries;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
export async function startProxyServer(proxyConfigPath?: string, localYarnConfigPath?: string, globalYarnConfigPath?: string, port: number =
|
|
92
|
+
export async function startProxyServer(proxyConfigPath?: string, localYarnConfigPath?: string, globalYarnConfigPath?: string, port: number = 0): Promise<Server> {
|
|
85
93
|
console.log('Starting proxy server...');
|
|
86
94
|
const registries = await loadRegistries(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
87
95
|
|
|
@@ -122,22 +130,57 @@ export async function startProxyServer(proxyConfigPath?: string, localYarnConfig
|
|
|
122
130
|
}
|
|
123
131
|
});
|
|
124
132
|
|
|
125
|
-
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
server.listen(port, () => {
|
|
135
|
+
const address: AddressInfo | string | null = server.address();
|
|
136
|
+
if (!address) {
|
|
137
|
+
console.error('Failed to get server address: address is null');
|
|
138
|
+
reject(new Error('Failed to get server address: address is null'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
if (typeof address === 'string') {
|
|
143
|
+
console.error('Server bound to a path (e.g., Unix socket), which is not supported');
|
|
144
|
+
reject(new Error('Server bound to a path, expected a TCP port'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const addressInfo: AddressInfo = address;
|
|
149
|
+
const actualPort: number = addressInfo.port;
|
|
131
150
|
|
|
132
|
-
|
|
151
|
+
// 从环境变量获取项目根目录,写入端口文件
|
|
152
|
+
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
153
|
+
const portFilePath = join(projectRoot, '.registry-proxy-port');
|
|
154
|
+
console.log(`Proxy server started at http://localhost:${actualPort}`);
|
|
155
|
+
writeFileSync(portFilePath, actualPort.toString(), 'utf8');
|
|
156
|
+
resolve(server);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
process.on('SIGTERM', () => {
|
|
160
|
+
console.log('Received SIGTERM, shutting down...');
|
|
161
|
+
server.close((err) => {
|
|
162
|
+
if (err) {
|
|
163
|
+
console.error('Error closing server:', err.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
console.log('Server closed.');
|
|
167
|
+
process.exit(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
console.error('Server did not close in time, forcing exit...');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}, 5000);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
133
176
|
}
|
|
134
177
|
|
|
135
178
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
136
179
|
const proxyConfigPath = process.argv[2];
|
|
137
180
|
const localYarnConfigPath = process.argv[3];
|
|
138
181
|
const globalYarnConfigPath = process.argv[4];
|
|
139
|
-
const port = parseInt(process.argv[5], 10) ||
|
|
140
|
-
console.log(`CLI: proxyConfigPath=${proxyConfigPath || './.registry-proxy.yml'}, localYarnConfigPath=${localYarnConfigPath || './.yarnrc.yml'}, globalYarnConfigPath=${globalYarnConfigPath || join(homedir(), '.yarnrc.yml')}, port=${port}`);
|
|
182
|
+
const port = parseInt(process.argv[5], 10) || 0;
|
|
183
|
+
console.log(`CLI: proxyConfigPath=${proxyConfigPath || './.registry-proxy.yml'}, localYarnConfigPath=${localYarnConfigPath || './.yarnrc.yml'}, globalYarnConfigPath=${globalYarnConfigPath || join(homedir(), '.yarnrc.yml')}, port=${port || 'dynamic'}`);
|
|
141
184
|
startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port).catch(err => {
|
|
142
185
|
console.error('Startup failed:', (err as Error).message);
|
|
143
186
|
process.exit(1);
|