com.jimuwd.xian.registry-proxy 1.0.4 → 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,21 +136,22 @@ com.jimuwd.xian.registry-proxy/
137
136
 
138
137
  ### 功能实现
139
138
  1. **配置加载(`loadRegistries`)**:
140
- - **代理配置文件**:从指定路径(默认 `./.registry-proxy.yml`)读取 `registries`,提取 `registryUrl` 和 `npmAuthToken`。
139
+ - **代理配置文件**:从指定路径(默认 `./.registry-proxy.yml`)读取 `registries`,支持 `~/` 路径解析。
141
140
  - **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(尤其是全局配置)进一步降低安全隐患。
141
+ - **Yarn 配置文件回退**:如果 `.registry-proxy.yml` 中 token 缺失,依次从本地 `.yarnrc.yml` 和全局 `~/.yarnrc.yml` 读取对应 `registryUrl` 的 `npmAuthToken`。
142
+ - **安全设计**:将 `registryUrl` 和 token 配置独立于 `.registry-proxy.yml`,避免敏感信息写入 Yarn 配置文件并提交到代码仓库。
144
143
  - **优先级**:`.registry-proxy.yml` token > 本地 `.yarnrc.yml` token > 全局 `~/.yarnrc.yml` token > 无 token。
145
144
  - **错误处理**:`.registry-proxy.yml` 必须存在且包含 `registries`,否则退出。
146
145
 
147
146
  2. **代理逻辑**:
148
- - **服务器**:使用 Node.js 的 `http.createServer` 创建本地 HTTP 服务,默认监听 `4873` 端口。
147
+ - **服务器**:使用 Node.js 的 `http.createServer` 创建本地 HTTP 服务,默认使用动态端口(`port = 0`)。
148
+ - **端口分配**:系统自动分配可用端口,并写入 `.registry-proxy-port` 文件。
149
149
  - **请求转发**:将所有请求按配置顺序转发到目标 registry,附带对应的 `Authorization: Bearer <token>`(如果存在)。
150
150
  - **Fallback**:依次尝试每个 registry,直到返回成功响应(`response.ok`)或全部失败(返回 404)。
151
151
 
152
152
  3. **进程管理**:
153
- - **优雅关闭**:监听 `SIGTERM` 信号,关闭服务器并退出进程。
154
- - **脚本集成**:通过 shell 脚本记录 PID,安装完成后发送 SIGTERM 停止服务。
153
+ - **优雅关闭**:监听 `SIGTERM` 信号,先关闭服务器再退出进程,5 秒超时强制退出。
154
+ - **脚本集成**:通过 shell 脚本动态运行代理,记录 PID 和端口,安装完成后停止服务。
155
155
 
156
156
  ### 技术栈
157
157
  - **语言**:TypeScript(ES Modules)。
@@ -163,50 +163,41 @@ com.jimuwd.xian.registry-proxy/
163
163
 
164
164
  ### CLI 参数
165
165
  ```bash
166
- registry-proxy [proxyConfigPath] [localYarnConfigPath] [globalYarnConfigPath] [port]
166
+ npx com.jimuwd.xian.registry-proxy [proxyConfigPath] [localYarnConfigPath] [globalYarnConfigPath] [port]
167
167
  ```
168
168
  - `proxyConfigPath`:代理配置文件路径,默认 `./.registry-proxy.yml`。
169
169
  - `localYarnConfigPath`:本地 Yarn 配置文件路径,默认 `./.yarnrc.yml`。
170
170
  - `globalYarnConfigPath`:全局 Yarn 配置文件路径,默认 `~/.yarnrc.yml`。
171
- - `port`:代理服务器端口,默认 `4873`。
171
+ - `port`:代理服务器端口,默认 `0`(动态分配)。
172
172
 
173
173
  示例:
174
174
  ```bash
175
- 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
176
176
  ```
177
177
 
178
178
  ### 配置说明
179
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
- ```
180
+ - 使用 `registries` 字段定义代理的 registry 列表。
181
+ - **格式要求**:每个 `registryUrl` 后必须跟一个对象(至少是 `{}`),否则解析为 `null` 会导致运行时错误。
187
182
  - 示例:
188
183
  ```yaml
189
184
  registries:
190
185
  "https://repo.jimuwd.com/jimuwd/~npm/":
191
186
  npmAuthToken: "private-token"
192
187
  ```
193
- - **注意**:无需配置 `npmAlwaysAuth`,此项仅适用于 Yarn 的 `.yarnrc.yml`,对代理行为无影响。
194
188
  - **`.yarnrc.yml`**:
195
- - 仅用于设置 `npmRegistryServer` 和回退 token。
196
- - 如果需要强制认证,可在回退的 Yarn 配置中添加 `npmAlwaysAuth: true`。
189
+ - 仅用于设置 `unsafeHttpWhitelist`,代理地址由脚本动态提供。
197
190
 
198
191
  ### 注意事项
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`,避免提交到代码仓库。
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
+ - 确保服务器关闭后进程退出,超时强制终止。
210
201
 
211
202
  ### 开发与发布
212
203
  1. **构建**:
@@ -221,25 +212,13 @@ yarn run registry-proxy ./custom-registry.yml ./custom-yarn.yml ~/.custom-yarn.y
221
212
  ---
222
213
 
223
214
  ### 测试流程
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. **运行**:
215
+ 1. **准备项目**:
216
+ - 项目目录包含 `.registry-proxy.yml`、`install-from-proxy-registries.sh` 和 `package.json`。
217
+ 2. **运行**:
243
218
  ```bash
244
- bash start-proxy.sh
219
+ cd project1 && yarn install &
220
+ cd project2 && yarn install &
245
221
  ```
222
+ 3. **验证**:
223
+ - 检查两个项目使用不同端口。
224
+ - 确保日志顺序正确,且进程正常退出(`ps aux | grep registry-proxy` 无残留)。
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,49 @@ 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
+ // 显式声明 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
+ });
112
148
  });
113
- return server;
114
149
  }
115
150
  if (import.meta.url === `file://${process.argv[1]}`) {
116
151
  const proxyConfigPath = process.argv[2];
117
152
  const localYarnConfigPath = process.argv[3];
118
153
  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}`);
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'}`);
121
156
  startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port).catch(err => {
122
157
  console.error('Startup failed:', err.message);
123
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.4",
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,59 +1,67 @@
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
 
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,54 @@ 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
+ }
131
147
 
132
- return server;
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
+ });
155
+
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
+ });
133
173
  }
134
174
 
135
175
  if (import.meta.url === `file://${process.argv[1]}`) {
136
176
  const proxyConfigPath = process.argv[2];
137
177
  const localYarnConfigPath = process.argv[3];
138
178
  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}`);
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'}`);
141
181
  startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port).catch(err => {
142
182
  console.error('Startup failed:', (err as Error).message);
143
183
  process.exit(1);