com.jimuwd.xian.registry-proxy 1.0.3 → 1.0.5

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