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 +68 -88
- package/dist/index.js +71 -25
- package/package.json +1 -1
- package/src/index.ts +79 -26
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
|
|
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
|
-
-
|
|
11
|
+
- **优雅关闭**:通过 SIGTERM 信号确保服务彻底停止。
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|
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.
|
|
48
|
+
1. **创建安装脚本 `install-from-proxy-registries.sh`**
|
|
60
49
|
在项目根目录添加以下脚本,用于启动代理并安装依赖:
|
|
61
50
|
```bash
|
|
62
51
|
#!/bin/bash
|
|
63
52
|
|
|
64
|
-
#
|
|
65
|
-
|
|
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
|
-
#
|
|
69
|
-
echo "Waiting for proxy server to start
|
|
58
|
+
# 等待代理服务器启动并写入端口,最多 10 秒
|
|
59
|
+
echo "Waiting for proxy server to start..."
|
|
70
60
|
for i in {1..100}; do
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
80
|
-
echo "Error: Proxy server failed to start
|
|
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
|
-
#
|
|
86
|
-
yarn install --registry http://localhost
|
|
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
|
-
"
|
|
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
|
|
117
|
-
Proxy server started at http://localhost:
|
|
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
|
|
141
|
-
- **
|
|
142
|
-
-
|
|
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
|
|
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
|
|
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`:代理服务器端口,默认 `
|
|
171
|
+
- `port`:代理服务器端口,默认 `0`(动态分配)。
|
|
171
172
|
|
|
172
173
|
示例:
|
|
173
174
|
```bash
|
|
174
|
-
|
|
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
|
|
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
|
-
- 仅用于设置 `
|
|
195
|
-
- 如果需要强制认证,可在回退的 Yarn 配置中添加 `npmAlwaysAuth: true`。
|
|
189
|
+
- 仅用于设置 `unsafeHttpWhitelist`,代理地址由脚本动态提供。
|
|
196
190
|
|
|
197
191
|
### 注意事项
|
|
198
|
-
1.
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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(
|
|
21
|
+
const proxyYamlContent = await readFile(resolvedProxyPath, 'utf8');
|
|
12
22
|
proxyConfig = load(proxyYamlContent);
|
|
13
|
-
console.log(`Loaded proxy config from ${
|
|
23
|
+
console.log(`Loaded proxy config from ${resolvedProxyPath}`);
|
|
14
24
|
}
|
|
15
25
|
catch (e) {
|
|
16
|
-
console.error(`Failed to load ${
|
|
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 ${
|
|
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(
|
|
35
|
+
const localYamlContent = await readFile(resolvedLocalYarnPath, 'utf8');
|
|
26
36
|
localYarnConfig = load(localYamlContent);
|
|
27
|
-
console.log(`Loaded local Yarn config from ${
|
|
37
|
+
console.log(`Loaded local Yarn config from ${resolvedLocalYarnPath}`);
|
|
28
38
|
}
|
|
29
39
|
catch (e) {
|
|
30
|
-
console.warn(`Failed to load ${
|
|
40
|
+
console.warn(`Failed to load ${resolvedLocalYarnPath}: ${e.message}`);
|
|
31
41
|
}
|
|
32
42
|
let globalYarnConfig = { npmRegistries: {} };
|
|
33
43
|
try {
|
|
34
|
-
const globalYamlContent = await readFile(
|
|
44
|
+
const globalYamlContent = await readFile(resolvedGlobalYarnPath, 'utf8');
|
|
35
45
|
globalYarnConfig = load(globalYamlContent);
|
|
36
|
-
console.log(`Loaded global Yarn config from ${
|
|
46
|
+
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
37
47
|
}
|
|
38
48
|
catch (e) {
|
|
39
|
-
console.warn(`Failed to load ${
|
|
49
|
+
console.warn(`Failed to load ${resolvedGlobalYarnPath}: ${e.message}`);
|
|
40
50
|
}
|
|
41
|
-
const
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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?.[
|
|
51
|
-
token = globalYarnConfig.npmRegistries[
|
|
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 =
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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) ||
|
|
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
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(
|
|
30
|
+
const proxyYamlContent = await readFile(resolvedProxyPath, 'utf8');
|
|
17
31
|
proxyConfig = load(proxyYamlContent) as ProxyConfig;
|
|
18
|
-
console.log(`Loaded proxy config from ${
|
|
32
|
+
console.log(`Loaded proxy config from ${resolvedProxyPath}`);
|
|
19
33
|
} catch (e) {
|
|
20
|
-
console.error(`Failed to load ${
|
|
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 ${
|
|
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(
|
|
45
|
+
const localYamlContent = await readFile(resolvedLocalYarnPath, 'utf8');
|
|
32
46
|
localYarnConfig = load(localYamlContent) as YarnConfig;
|
|
33
|
-
console.log(`Loaded local Yarn config from ${
|
|
47
|
+
console.log(`Loaded local Yarn config from ${resolvedLocalYarnPath}`);
|
|
34
48
|
} catch (e) {
|
|
35
|
-
console.warn(`Failed to load ${
|
|
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(
|
|
54
|
+
const globalYamlContent = await readFile(resolvedGlobalYarnPath, 'utf8');
|
|
41
55
|
globalYarnConfig = load(globalYamlContent) as YarnConfig;
|
|
42
|
-
console.log(`Loaded global Yarn config from ${
|
|
56
|
+
console.log(`Loaded global Yarn config from ${resolvedGlobalYarnPath}`);
|
|
43
57
|
} catch (e) {
|
|
44
|
-
console.warn(`Failed to load ${
|
|
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 =
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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?.[
|
|
60
|
-
token = globalYarnConfig.npmRegistries[
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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) ||
|
|
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);
|