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 CHANGED
@@ -1,245 +1,732 @@
1
+ # Registry Proxy
1
2
 
2
- # README.md
3
-
4
- ## 项目简介
5
-
6
- `com.jimuwd.xian.registry-proxy` 是一个轻量级的 npm 代理服务器,旨在为 Node.js 项目提供多 registry 的代理和 fallback 机制。它通过读取独立的配置文件 `.registry-proxy.yml`,将多个 npm registry(如公共 registry、私有仓库等)代理到一个本地端口(默认 `4873`),并支持在 `.registry-proxy.yml` 中 token 缺失时从 Yarn 配置文件(本地 `.yarnrc.yml` 和全局 `~/.yarnrc.yml`)回退获取认证信息。
7
- 主要功能:
8
- - **多 registry 代理**:将多个 npm registry 统一代理到本地端口,提供单一访问入口。
9
- - **Fallback 机制**:按配置顺序尝试多个 registry,直到找到可用包。
10
- - **灵活的 token 管理**:优先使用 `.registry-proxy.yml` 中的 `npmAuthToken`,缺失时从 Yarn 配置文件读取。
11
- - **优雅关闭**:支持通过 SIGTERM 信号优雅停止服务。
12
-
13
- 这个工具特别适合需要同时访问多个 npm 源的开发场景,尤其在 CI/CD 或本地开发中,能提高依赖安装的稳定性和效率。
14
-
15
- ---
16
-
17
- ## 快速上手指南
18
-
19
- ### 安装
20
- 在你的业务项目中,将 `com.jimuwd.xian.registry-proxy` 添加为开发依赖。假设你的私有 Yarn 仓库地址为 `https://repo.jimuwd.com/jimuwd/~npm/`:
21
-
22
- ```bash
23
- yarn add --dev com.jimuwd.xian.registry-proxy --registry https://repo.jimuwd.com/jimuwd/~npm/
24
- ```
25
-
26
- ### 配置
27
- 1. **代理配置文件 `.registry-proxy.yml`**
28
- 在业务项目根目录创建 `.registry-proxy.yml`,指定需要代理的 registry 列表。每个 registry 必须至少是一个空对象 `{}`,否则会导致解析错误:
29
- ```yaml
30
- registries:
31
- "http://localhost:4873/":
32
- npmAuthToken: "local-token" # 可选
33
- "https://registry.npmjs.org/":
34
- {} # 无 token 时使用空对象
35
- "https://repo.jimuwd.com/jimuwd/~npm/":
36
- npmAuthToken: "private-token" # 可选
37
- ```
38
-
39
- 2. **本地 `.yarnrc.yml`**
40
- 在项目根目录创建或编辑 `.yarnrc.yml`,指定 Yarn 使用本地代理地址:
41
- ```yaml
42
- npmRegistryServer: "http://localhost:4873/"
43
- unsafeHttpWhitelist:
44
- - "localhost"
45
- ```
46
-
47
- 3. **全局 `~/.yarnrc.yml`(可选)**
48
- 如果 `.registry-proxy.yml` 未提供某些 registry 的 token,可以在用户主目录下的 `.yarnrc.yml` 配置回退 token:
49
- ```yaml
50
- npmRegistries:
51
- "https://registry.npmjs.org/":
52
- npmAuthToken: "global-npm-token"
53
- "https://repo.jimuwd.com/jimuwd/~npm/":
54
- npmAuthToken: "global-private-token"
55
- npmAlwaysAuth: true # 可选,控制 Yarn 行为
56
- ```
57
-
58
- ### 使用
59
- 1. **创建启动脚本 `start-proxy.sh`**
60
- 在项目根目录添加以下脚本,用于启动代理并安装依赖:
61
- ```bash
62
- #!/bin/bash
63
-
64
- # 启动代理服务器并记录 PID
65
- yarn run registry-proxy .registry-proxy.yml .yarnrc.yml ~/.yarnrc.yml &
66
- PROXY_PID=$!
67
-
68
- # 等待代理服务器启动,最多 10 秒
69
- echo "Waiting for proxy server to start on port 4873..."
70
- for i in {1..100}; do
71
- if nc -z localhost 4873 2>/dev/null; then
72
- echo "Proxy server is ready!"
73
- break
74
- fi
75
- sleep 0.1
76
- done
77
-
78
- # 检查是否成功启动
79
- if ! nc -z localhost 4873 2>/dev/null; then
80
- echo "Error: Proxy server failed to start on port 4873"
81
- kill $PROXY_PID
82
- exit 1
83
- fi
84
-
85
- # 使用本地代理运行 yarn install
86
- yarn install --registry http://localhost:4873/
87
-
88
- # 停止代理服务器
89
- echo "Stopping proxy server..."
90
- kill -TERM $PROXY_PID
91
- wait $PROXY_PID 2>/dev/null
92
- echo "Proxy server stopped."
93
- ```
94
-
95
- 2. **添加 Yarn 脚本**
96
- `package.json` 中定义快捷命令:
97
- ```json
98
- {
99
- "scripts": {
100
- "start-proxy": "registry-proxy .registry-proxy.yml .yarnrc.yml ~/.yarnrc.yml",
101
- "install": "bash start-proxy.sh"
102
- }
103
- }
104
- ```
105
-
106
- 3. **运行**
107
- 执行以下命令启动代理并安装依赖:
108
- ```bash
109
- yarn install
110
- ```
111
- - 代理会在安装完成后自动停止。
112
-
113
- ### 输出示例
114
- 运行后,你会看到类似以下输出:
115
- ```
116
- Waiting for proxy server to start on port 4873...
117
- Proxy server started at http://localhost:4873
118
- Proxy server is ready!
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
- com.jimuwd.xian.registry-proxy/
131
- ├── src/
132
- │ └── index.ts # 主逻辑:代理服务器实现
133
- ├── package.json # 项目配置和依赖
134
- ├── tsconfig.json # TypeScript 配置
135
- └── dist/ # 编译后的输出目录
136
- ```
137
-
138
- ### 功能实现
139
- 1. **配置加载(`loadRegistries`)**:
140
- - **代理配置文件**:从指定路径(默认 `./.registry-proxy.yml`)读取 `registries`,提取 `registryUrl` 和 `npmAuthToken`。
141
- - **URL 规范化**:自动去除 `registryUrl` 尾部斜杠,确保 `https://example.com/path/` 和 `https://example.com/path` 被视为同一地址,后配置覆盖前配置。
142
- - **Yarn 配置文件回退**:如果 `.registry-proxy.yml` 中 token 缺失,依次从本地 `.yarnrc.yml`(默认 `./.yarnrc.yml`)和全局 `~/.yarnrc.yml`(默认 `~/.yarnrc.yml`)读取对应 `registryUrl` 的 `npmAuthToken`。
143
- - **安全设计**:将 `registryUrl` 和 token 配置独立于 `.registry-proxy.yml`,避免敏感信息直接写入 Yarn 配置文件并提交到代码仓库。回退到 Yarn 配置的 token(尤其是全局配置)进一步降低安全隐患。
144
- - **优先级**:`.registry-proxy.yml` token > 本地 `.yarnrc.yml` token > 全局 `~/.yarnrc.yml` token > 无 token。
145
- - **错误处理**:`.registry-proxy.yml` 必须存在且包含 `registries`,否则退出。
146
-
147
- 2. **代理逻辑**:
148
- - **服务器**:使用 Node.js `http.createServer` 创建本地 HTTP 服务,默认监听 `4873` 端口。
149
- - **请求转发**:将所有请求按配置顺序转发到目标 registry,附带对应的 `Authorization: Bearer <token>`(如果存在)。
150
- - **Fallback**:依次尝试每个 registry,直到返回成功响应(`response.ok`)或全部失败(返回 404)。
151
-
152
- 3. **进程管理**:
153
- - **优雅关闭**:监听 `SIGTERM` 信号,关闭服务器并退出进程。
154
- - **脚本集成**:通过 shell 脚本记录 PID,安装完成后发送 SIGTERM 停止服务。
155
-
156
- ### 技术栈
157
- - **语言**:TypeScript(ES Modules)。
158
- - **模块系统**:`"module": "nodenext"`,兼容 Node.js v20+。
159
- - **依赖**:
160
- - `node-fetch@^3.3.2`:发起 HTTP 请求。
161
- - `js-yaml@^4.1.0`:解析 `.registry-proxy.yml` 和 `.yarnrc.yml` 文件。
162
- - **Node.js 版本**:推荐 v14+,测试于 v20.17.0。
163
-
164
- ### CLI 参数
165
- ```bash
166
- registry-proxy [proxyConfigPath] [localYarnConfigPath] [globalYarnConfigPath] [port]
167
- ```
168
- - `proxyConfigPath`:代理配置文件路径,默认 `./.registry-proxy.yml`。
169
- - `localYarnConfigPath`:本地 Yarn 配置文件路径,默认 `./.yarnrc.yml`。
170
- - `globalYarnConfigPath`:全局 Yarn 配置文件路径,默认 `~/.yarnrc.yml`。
171
- - `port`:代理服务器端口,默认 `4873`。
172
-
173
- 示例:
174
- ```bash
175
- yarn run registry-proxy ./custom-registry.yml ./custom-yarn.yml ~/.custom-yarn.yml 54321
176
- ```
177
-
178
- ### 配置说明
179
- - **`.registry-proxy.yml`**:
180
- - 使用 `registries` 字段定义代理的 registry 列表,与 Yarn 的 `npmRegistries` 区分。
181
- - **格式要求**:每个 `registryUrl` 后必须跟一个对象(至少是 `{}`),否则解析为 `null` 会导致运行时错误。例如:
182
- ```yaml
183
- registries:
184
- "https://repo.jimuwd.com/some/project/path/~npm/": {} # 正确
185
- "https://repo.jimuwd.com/some/project/path1/~npm/": # 错误,会解析为 null
186
- ```
187
- - 示例:
188
- ```yaml
189
- registries:
190
- "https://repo.jimuwd.com/jimuwd/~npm/":
191
- npmAuthToken: "private-token"
192
- ```
193
- - **注意**:无需配置 `npmAlwaysAuth`,此项仅适用于 Yarn 的 `.yarnrc.yml`,对代理行为无影响。
194
- - **`.yarnrc.yml`**:
195
- - 仅用于设置 `npmRegistryServer` 和回退 token。
196
- - 如果需要强制认证,可在回退的 Yarn 配置中添加 `npmAlwaysAuth: true`。
197
-
198
- ### 注意事项
199
- 1. **端口冲突**:
200
- - 默认端口 `4873` 是 Verdaccio 的惯用端口,可能与其他工具冲突。
201
- - 检查端口占用:`lsof -i :4873`。
202
- - 可通过参数指定其他端口(如 `54321`)。
203
- 2. **配置文件格式**:
204
- - 确保 `.registry-proxy.yml` 包含 `registries` 字段,`.yarnrc.yml` 包含 `npmRegistryServer`。
205
- 3. **日志**:
206
- - 当前通过 `console.log` 输出启动信息,可扩展为文件日志。
207
- 4. **安全性**:
208
- - 代理运行于本地,未开放外部访问,确保 `unsafeHttpWhitelist` 配置正确。
209
- - 优先将 token 放入 `.registry-proxy.yml` 或全局 `.yarnrc.yml`,避免提交到代码仓库。
210
-
211
- ### 开发与发布
212
- 1. **构建**:
213
- ```bash
214
- yarn build
215
- ```
216
- 2. **发布到私有仓库**:
217
- ```bash
218
- yarn publish --registry https://repo.jimuwd.com/jimuwd/~npm/
219
- ```
220
-
221
- ---
222
-
223
- ### 测试流程
224
- 1. **更新 `.registry-proxy.yml`**:
225
- ```yaml
226
- registries:
227
- "https://repo.jimuwd.com/jimuwd/~npm/": {}
228
- "https://registry.npmjs.org/": {}
229
- ```
230
- 2. **构建并发布**:
231
- ```bash
232
- cd registry-proxy
233
- yarn install
234
- yarn build
235
- yarn publish --registry https://repo.jimuwd.com/jimuwd/~npm/
236
- ```
237
- 3. **更新业务项目**:
238
- ```bash
239
- cd your-business-project
240
- yarn add com.jimuwd.xian.registry-proxy@latest --registry https://repo.jimuwd.com/jimuwd/~npm/
241
- ```
242
- 4. **运行**:
243
- ```bash
244
- bash start-proxy.sh
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
- // 规范化 URL,去除尾部斜杠
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(proxyConfigPath, 'utf8');
21
+ const proxyYamlContent = await readFile(resolvedProxyPath, 'utf8');
16
22
  proxyConfig = load(proxyYamlContent);
17
- console.log(`Loaded proxy config from ${proxyConfigPath}`);
23
+ console.log(`Loaded proxy config from ${resolvedProxyPath}`);
18
24
  }
19
25
  catch (e) {
20
- console.error(`Failed to load ${proxyConfigPath}: ${e.message}`);
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 ${proxyConfigPath}`);
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(localYarnConfigPath, 'utf8');
35
+ const localYamlContent = await readFile(resolvedLocalYarnPath, 'utf8');
30
36
  localYarnConfig = load(localYamlContent);
31
- console.log(`Loaded local Yarn config from ${localYarnConfigPath}`);
37
+ console.log(`Loaded local Yarn config from ${resolvedLocalYarnPath}`);
32
38
  }
33
39
  catch (e) {
34
- console.warn(`Failed to load ${localYarnConfigPath}: ${e.message}`);
40
+ console.warn(`Failed to load ${resolvedLocalYarnPath}: ${e.message}`);
35
41
  }
36
42
  let globalYarnConfig = { npmRegistries: {} };
37
43
  try {
38
- const globalYamlContent = await readFile(globalYarnConfigPath, 'utf8');
44
+ const globalYamlContent = await readFile(resolvedGlobalYarnPath, 'utf8');
39
45
  globalYarnConfig = load(globalYamlContent);
40
- console.log(`Loaded global Yarn config from ${globalYarnConfigPath}`);
46
+ console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
41
47
  }
42
48
  catch (e) {
43
- console.warn(`Failed to load ${globalYarnConfigPath}: ${e.message}`);
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 ${proxyConfigPath}, using local Yarn config`);
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 = 4873) {
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
- server.listen(port, () => console.log(`Proxy server started at http://localhost:${port}`));
109
- process.on('SIGTERM', () => {
110
- console.log('Received SIGTERM, shutting down...');
111
- server.close(() => process.exit(0));
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) || 4873;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "A lightweight npm registry proxy with fallback support",
6
6
  "main": "dist/index.js",
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(proxyConfigPath, 'utf8');
30
+ const proxyYamlContent = await readFile(resolvedProxyPath, 'utf8');
22
31
  proxyConfig = load(proxyYamlContent) as ProxyConfig;
23
- console.log(`Loaded proxy config from ${proxyConfigPath}`);
32
+ console.log(`Loaded proxy config from ${resolvedProxyPath}`);
24
33
  } catch (e) {
25
- console.error(`Failed to load ${proxyConfigPath}: ${(e as Error).message}`);
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 ${proxyConfigPath}`);
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(localYarnConfigPath, 'utf8');
45
+ const localYamlContent = await readFile(resolvedLocalYarnPath, 'utf8');
37
46
  localYarnConfig = load(localYamlContent) as YarnConfig;
38
- console.log(`Loaded local Yarn config from ${localYarnConfigPath}`);
47
+ console.log(`Loaded local Yarn config from ${resolvedLocalYarnPath}`);
39
48
  } catch (e) {
40
- console.warn(`Failed to load ${localYarnConfigPath}: ${(e as Error).message}`);
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(globalYarnConfigPath, 'utf8');
54
+ const globalYamlContent = await readFile(resolvedGlobalYarnPath, 'utf8');
46
55
  globalYarnConfig = load(globalYamlContent) as YarnConfig;
47
- console.log(`Loaded global Yarn config from ${globalYarnConfigPath}`);
56
+ console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
48
57
  } catch (e) {
49
- console.warn(`Failed to load ${globalYarnConfigPath}: ${(e as Error).message}`);
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 ${proxyConfigPath}, using local Yarn config`);
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 = 4873): Promise<Server> {
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
- server.listen(port, () => console.log(`Proxy server started at http://localhost:${port}`));
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
- process.on('SIGTERM', () => {
128
- console.log('Received SIGTERM, shutting down...');
129
- server.close(() => process.exit(0));
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
- return server;
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) || 4873;
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);