com.jimuwd.xian.registry-proxy 1.0.13 → 1.0.14
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/dist/index.js +85 -14
- package/package.json +1 -1
- package/src/index.ts +102 -31
package/dist/index.js
CHANGED
|
@@ -6,8 +6,36 @@ import { load } from 'js-yaml';
|
|
|
6
6
|
import fetch from 'node-fetch';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { join, resolve } from 'path';
|
|
9
|
-
import { URL } from 'url';
|
|
9
|
+
import { URL } from 'url';
|
|
10
10
|
const { readFile, writeFile } = fsPromises;
|
|
11
|
+
// 并发控制队列
|
|
12
|
+
class ConcurrencyLimiter {
|
|
13
|
+
maxConcurrency;
|
|
14
|
+
current = 0;
|
|
15
|
+
queue = [];
|
|
16
|
+
constructor(maxConcurrency) {
|
|
17
|
+
this.maxConcurrency = maxConcurrency;
|
|
18
|
+
}
|
|
19
|
+
async acquire() {
|
|
20
|
+
if (this.current < this.maxConcurrency) {
|
|
21
|
+
this.current++;
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
this.queue.push(resolve);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
release() {
|
|
29
|
+
this.current--;
|
|
30
|
+
const next = this.queue.shift();
|
|
31
|
+
if (next) {
|
|
32
|
+
this.current++;
|
|
33
|
+
next();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 设置最大并发数(可调整)
|
|
38
|
+
const limiter = new ConcurrencyLimiter(5); // 限制为 5 个并发请求
|
|
11
39
|
function normalizeUrl(url) {
|
|
12
40
|
try {
|
|
13
41
|
const urlObj = new URL(url);
|
|
@@ -20,6 +48,7 @@ function normalizeUrl(url) {
|
|
|
20
48
|
if (!urlObj.pathname.endsWith('/')) {
|
|
21
49
|
urlObj.pathname += '/';
|
|
22
50
|
}
|
|
51
|
+
console.debug(`Normalized URL: ${url} -> ${urlObj.toString()}`);
|
|
23
52
|
return urlObj.toString();
|
|
24
53
|
}
|
|
25
54
|
catch (e) {
|
|
@@ -28,7 +57,9 @@ function normalizeUrl(url) {
|
|
|
28
57
|
}
|
|
29
58
|
}
|
|
30
59
|
function resolvePath(path) {
|
|
31
|
-
|
|
60
|
+
const resolved = path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
61
|
+
console.debug(`Resolved path: ${path} -> ${resolved}`);
|
|
62
|
+
return resolved;
|
|
32
63
|
}
|
|
33
64
|
function removeRegistryPrefix(tarballUrl, registries) {
|
|
34
65
|
try {
|
|
@@ -36,25 +67,31 @@ function removeRegistryPrefix(tarballUrl, registries) {
|
|
|
36
67
|
const normalizedRegistries = registries
|
|
37
68
|
.map(r => normalizeUrl(r.url))
|
|
38
69
|
.sort((a, b) => b.length - a.length);
|
|
70
|
+
console.debug(`Removing registry prefix from tarball: ${normalizedTarball}`);
|
|
39
71
|
for (const registry of normalizedRegistries) {
|
|
40
72
|
if (normalizedTarball.startsWith(registry)) {
|
|
41
|
-
|
|
73
|
+
const result = normalizedTarball.slice(registry.length - 1) || '/';
|
|
74
|
+
console.debug(`Matched registry ${registry}, result: ${result}`);
|
|
75
|
+
return result;
|
|
42
76
|
}
|
|
43
77
|
}
|
|
78
|
+
console.debug(`No registry prefix matched for ${normalizedTarball}`);
|
|
44
79
|
}
|
|
45
80
|
catch (e) {
|
|
46
|
-
console.error(`Invalid URL: ${tarballUrl}`, e);
|
|
81
|
+
console.error(`Invalid URL in removeRegistryPrefix: ${tarballUrl}`, e);
|
|
47
82
|
}
|
|
48
83
|
return tarballUrl;
|
|
49
84
|
}
|
|
50
85
|
async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
|
|
51
86
|
const resolvedPath = resolvePath(proxyConfigPath);
|
|
87
|
+
console.debug(`Loading proxy config from: ${resolvedPath}`);
|
|
52
88
|
try {
|
|
53
89
|
const content = await readFile(resolvedPath, 'utf8');
|
|
54
90
|
const config = load(content);
|
|
55
91
|
if (!config.registries) {
|
|
56
92
|
throw new Error('Missing required "registries" field in config');
|
|
57
93
|
}
|
|
94
|
+
console.debug('Loaded proxy config:', JSON.stringify(config, null, 2));
|
|
58
95
|
return config;
|
|
59
96
|
}
|
|
60
97
|
catch (e) {
|
|
@@ -63,9 +100,12 @@ async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
|
|
|
63
100
|
}
|
|
64
101
|
}
|
|
65
102
|
async function loadYarnConfig(path) {
|
|
103
|
+
console.debug(`Loading Yarn config from: ${path}`);
|
|
66
104
|
try {
|
|
67
105
|
const content = await readFile(resolvePath(path), 'utf8');
|
|
68
|
-
|
|
106
|
+
const config = load(content);
|
|
107
|
+
console.debug(`Loaded Yarn config from ${path}:`, JSON.stringify(config, null, 2));
|
|
108
|
+
return config;
|
|
69
109
|
}
|
|
70
110
|
catch (e) {
|
|
71
111
|
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
@@ -73,6 +113,7 @@ async function loadYarnConfig(path) {
|
|
|
73
113
|
}
|
|
74
114
|
}
|
|
75
115
|
async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYarnConfigPath = './.yarnrc.yml', globalYarnConfigPath = join(homedir(), '.yarnrc.yml')) {
|
|
116
|
+
console.debug('Loading registries...');
|
|
76
117
|
const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
|
|
77
118
|
loadProxyConfig(proxyConfigPath),
|
|
78
119
|
loadYarnConfig(localYarnConfigPath),
|
|
@@ -89,13 +130,16 @@ async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYa
|
|
|
89
130
|
config.npmRegistries?.[url];
|
|
90
131
|
if (registryConfig?.npmAuthToken) {
|
|
91
132
|
token = registryConfig.npmAuthToken;
|
|
133
|
+
console.debug(`Found token for ${normalizedUrl} in Yarn config`);
|
|
92
134
|
break;
|
|
93
135
|
}
|
|
94
136
|
}
|
|
95
137
|
}
|
|
96
138
|
registryMap.set(normalizedUrl, { url: normalizedUrl, token });
|
|
97
139
|
}
|
|
98
|
-
|
|
140
|
+
const registries = Array.from(registryMap.values());
|
|
141
|
+
console.log('Loaded registries:', registries);
|
|
142
|
+
return registries;
|
|
99
143
|
}
|
|
100
144
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
101
145
|
const proxyConfig = await loadProxyConfig(proxyConfigPath);
|
|
@@ -106,12 +150,16 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
106
150
|
console.log('HTTPS:', !!proxyConfig.https);
|
|
107
151
|
let proxyPort;
|
|
108
152
|
const requestHandler = async (req, res) => {
|
|
153
|
+
console.debug(`Received request: ${req.method} ${req.url}`);
|
|
109
154
|
if (!req.url || !req.headers.host) {
|
|
155
|
+
console.error('Invalid request: missing URL or host header');
|
|
110
156
|
res.writeHead(400).end('Invalid Request');
|
|
111
157
|
return;
|
|
112
158
|
}
|
|
113
159
|
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
160
|
+
console.debug(`Full URL: ${fullUrl.toString()}`);
|
|
114
161
|
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
162
|
+
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePath}`);
|
|
115
163
|
res.writeHead(404).end('Not Found');
|
|
116
164
|
return;
|
|
117
165
|
}
|
|
@@ -119,7 +167,8 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
119
167
|
? fullUrl.pathname.slice(basePath.length)
|
|
120
168
|
: fullUrl.pathname;
|
|
121
169
|
console.log(`Proxying: ${relativePath}`);
|
|
122
|
-
const
|
|
170
|
+
const fetchPromises = registries.map(async ({ url, token }) => {
|
|
171
|
+
await limiter.acquire(); // 获取并发许可
|
|
123
172
|
try {
|
|
124
173
|
const cleanRelativePath = relativePath.replace(/\/+$/, '');
|
|
125
174
|
const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
|
|
@@ -127,13 +176,18 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
127
176
|
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
128
177
|
const response = await fetch(targetUrl, { headers });
|
|
129
178
|
console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
|
|
179
|
+
console.debug(`Response headers from ${url}:`, Object.fromEntries(response.headers.entries()));
|
|
130
180
|
return response.ok ? response : null;
|
|
131
181
|
}
|
|
132
182
|
catch (e) {
|
|
133
183
|
console.error(`Failed to fetch from ${url}:`, e);
|
|
134
184
|
return null;
|
|
135
185
|
}
|
|
136
|
-
|
|
186
|
+
finally {
|
|
187
|
+
limiter.release(); // 释放并发许可
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const responses = await Promise.all(fetchPromises);
|
|
137
191
|
const successResponse = responses.find((r) => r !== null);
|
|
138
192
|
if (!successResponse) {
|
|
139
193
|
console.error(`All registries failed for ${relativePath}`);
|
|
@@ -141,21 +195,28 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
141
195
|
return;
|
|
142
196
|
}
|
|
143
197
|
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
198
|
+
console.debug(`Content-Type: ${contentType}`);
|
|
144
199
|
if (contentType.includes('application/json')) {
|
|
145
200
|
try {
|
|
146
201
|
const data = await successResponse.json();
|
|
202
|
+
console.debug('JSON response data:', JSON.stringify(data, null, 2));
|
|
147
203
|
if (data.versions) {
|
|
148
204
|
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
205
|
+
console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
|
|
149
206
|
for (const version in data.versions) {
|
|
150
207
|
const dist = data.versions[version]?.dist;
|
|
151
208
|
if (dist?.tarball) {
|
|
152
209
|
const originalUrl = new URL(dist.tarball);
|
|
153
210
|
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
154
211
|
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
212
|
+
console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
|
|
155
213
|
}
|
|
156
214
|
}
|
|
157
215
|
}
|
|
158
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
216
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
217
|
+
const jsonResponse = JSON.stringify(data);
|
|
218
|
+
console.debug(`Sending JSON response: ${jsonResponse}`);
|
|
219
|
+
res.end(jsonResponse);
|
|
159
220
|
}
|
|
160
221
|
catch (e) {
|
|
161
222
|
console.error('Failed to parse JSON response:', e);
|
|
@@ -172,24 +233,31 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
172
233
|
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
173
234
|
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
174
235
|
};
|
|
236
|
+
console.debug(`Streaming response with headers:`, safeHeaders);
|
|
175
237
|
res.writeHead(successResponse.status, safeHeaders);
|
|
176
|
-
successResponse.body.pipe(res)
|
|
238
|
+
successResponse.body.pipe(res).on('error', (err) => {
|
|
239
|
+
console.error(`Stream error for ${relativePath}:`, err);
|
|
240
|
+
res.writeHead(502).end('Stream Error');
|
|
241
|
+
});
|
|
177
242
|
}
|
|
178
243
|
};
|
|
179
244
|
let server;
|
|
180
245
|
if (proxyConfig.https) {
|
|
181
246
|
const { key, cert } = proxyConfig.https;
|
|
247
|
+
const keyPath = resolvePath(key);
|
|
248
|
+
const certPath = resolvePath(cert);
|
|
249
|
+
console.debug(`Loading HTTPS key: ${keyPath}, cert: ${certPath}`);
|
|
182
250
|
try {
|
|
183
|
-
await fsPromises.access(
|
|
184
|
-
await fsPromises.access(
|
|
251
|
+
await fsPromises.access(keyPath);
|
|
252
|
+
await fsPromises.access(certPath);
|
|
185
253
|
}
|
|
186
254
|
catch (e) {
|
|
187
255
|
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
188
256
|
process.exit(1);
|
|
189
257
|
}
|
|
190
258
|
const httpsOptions = {
|
|
191
|
-
key: readFileSync(
|
|
192
|
-
cert: readFileSync(
|
|
259
|
+
key: readFileSync(keyPath),
|
|
260
|
+
cert: readFileSync(certPath),
|
|
193
261
|
};
|
|
194
262
|
server = createHttpsServer(httpsOptions, requestHandler);
|
|
195
263
|
}
|
|
@@ -202,12 +270,14 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
202
270
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
203
271
|
process.exit(1);
|
|
204
272
|
}
|
|
273
|
+
console.error('Server error:', err);
|
|
205
274
|
reject(err);
|
|
206
275
|
});
|
|
207
276
|
server.listen(port, () => {
|
|
208
277
|
const address = server.address();
|
|
209
278
|
proxyPort = address.port;
|
|
210
279
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
280
|
+
console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
|
|
211
281
|
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
212
282
|
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
213
283
|
resolve(server);
|
|
@@ -216,6 +286,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
216
286
|
}
|
|
217
287
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
218
288
|
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
289
|
+
console.debug(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
|
|
219
290
|
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
220
291
|
console.error('Failed to start server:', err);
|
|
221
292
|
process.exit(1);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { load } from 'js-yaml';
|
|
|
7
7
|
import fetch, { Response } from 'node-fetch';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
import { join, resolve } from 'path';
|
|
10
|
-
import { URL } from 'url';
|
|
10
|
+
import { URL } from 'url';
|
|
11
11
|
|
|
12
12
|
const { readFile, writeFile } = fsPromises;
|
|
13
13
|
|
|
@@ -23,6 +23,39 @@ interface RegistryInfo { url: string; token?: string; }
|
|
|
23
23
|
interface PackageVersion { dist?: { tarball?: string }; }
|
|
24
24
|
interface PackageData { versions?: Record<string, PackageVersion>; }
|
|
25
25
|
|
|
26
|
+
// 并发控制队列
|
|
27
|
+
class ConcurrencyLimiter {
|
|
28
|
+
private maxConcurrency: number;
|
|
29
|
+
private current: number = 0;
|
|
30
|
+
private queue: Array<() => void> = [];
|
|
31
|
+
|
|
32
|
+
constructor(maxConcurrency: number) {
|
|
33
|
+
this.maxConcurrency = maxConcurrency;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async acquire(): Promise<void> {
|
|
37
|
+
if (this.current < this.maxConcurrency) {
|
|
38
|
+
this.current++;
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
}
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
this.queue.push(resolve);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
release(): void {
|
|
47
|
+
this.current--;
|
|
48
|
+
const next = this.queue.shift();
|
|
49
|
+
if (next) {
|
|
50
|
+
this.current++;
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 设置最大并发数(可调整)
|
|
57
|
+
const limiter = new ConcurrencyLimiter(5); // 限制为 5 个并发请求
|
|
58
|
+
|
|
26
59
|
function normalizeUrl(url: string): string {
|
|
27
60
|
try {
|
|
28
61
|
const urlObj = new URL(url);
|
|
@@ -34,6 +67,7 @@ function normalizeUrl(url: string): string {
|
|
|
34
67
|
if (!urlObj.pathname.endsWith('/')) {
|
|
35
68
|
urlObj.pathname += '/';
|
|
36
69
|
}
|
|
70
|
+
console.debug(`Normalized URL: ${url} -> ${urlObj.toString()}`);
|
|
37
71
|
return urlObj.toString();
|
|
38
72
|
} catch (e) {
|
|
39
73
|
console.error(`Invalid URL: ${url}`, e);
|
|
@@ -42,7 +76,9 @@ function normalizeUrl(url: string): string {
|
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
function resolvePath(path: string): string {
|
|
45
|
-
|
|
79
|
+
const resolved = path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
80
|
+
console.debug(`Resolved path: ${path} -> ${resolved}`);
|
|
81
|
+
return resolved;
|
|
46
82
|
}
|
|
47
83
|
|
|
48
84
|
function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
|
|
@@ -52,27 +88,31 @@ function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): s
|
|
|
52
88
|
.map(r => normalizeUrl(r.url))
|
|
53
89
|
.sort((a, b) => b.length - a.length);
|
|
54
90
|
|
|
91
|
+
console.debug(`Removing registry prefix from tarball: ${normalizedTarball}`);
|
|
55
92
|
for (const registry of normalizedRegistries) {
|
|
56
93
|
if (normalizedTarball.startsWith(registry)) {
|
|
57
|
-
|
|
94
|
+
const result = normalizedTarball.slice(registry.length - 1) || '/';
|
|
95
|
+
console.debug(`Matched registry ${registry}, result: ${result}`);
|
|
96
|
+
return result;
|
|
58
97
|
}
|
|
59
98
|
}
|
|
99
|
+
console.debug(`No registry prefix matched for ${normalizedTarball}`);
|
|
60
100
|
} catch (e) {
|
|
61
|
-
console.error(`Invalid URL: ${tarballUrl}`, e);
|
|
101
|
+
console.error(`Invalid URL in removeRegistryPrefix: ${tarballUrl}`, e);
|
|
62
102
|
}
|
|
63
103
|
return tarballUrl;
|
|
64
104
|
}
|
|
65
105
|
|
|
66
|
-
async function loadProxyConfig(
|
|
67
|
-
proxyConfigPath = './.registry-proxy.yml'
|
|
68
|
-
): Promise<ProxyConfig> {
|
|
106
|
+
async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml'): Promise<ProxyConfig> {
|
|
69
107
|
const resolvedPath = resolvePath(proxyConfigPath);
|
|
108
|
+
console.debug(`Loading proxy config from: ${resolvedPath}`);
|
|
70
109
|
try {
|
|
71
110
|
const content = await readFile(resolvedPath, 'utf8');
|
|
72
111
|
const config = load(content) as ProxyConfig;
|
|
73
112
|
if (!config.registries) {
|
|
74
113
|
throw new Error('Missing required "registries" field in config');
|
|
75
114
|
}
|
|
115
|
+
console.debug('Loaded proxy config:', JSON.stringify(config, null, 2));
|
|
76
116
|
return config;
|
|
77
117
|
} catch (e) {
|
|
78
118
|
console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
@@ -81,9 +121,12 @@ async function loadProxyConfig(
|
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
async function loadYarnConfig(path: string): Promise<YarnConfig> {
|
|
124
|
+
console.debug(`Loading Yarn config from: ${path}`);
|
|
84
125
|
try {
|
|
85
126
|
const content = await readFile(resolvePath(path), 'utf8');
|
|
86
|
-
|
|
127
|
+
const config = load(content) as YarnConfig;
|
|
128
|
+
console.debug(`Loaded Yarn config from ${path}:`, JSON.stringify(config, null, 2));
|
|
129
|
+
return config;
|
|
87
130
|
} catch (e) {
|
|
88
131
|
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
89
132
|
return {};
|
|
@@ -95,6 +138,7 @@ async function loadRegistries(
|
|
|
95
138
|
localYarnConfigPath = './.yarnrc.yml',
|
|
96
139
|
globalYarnConfigPath = join(homedir(), '.yarnrc.yml')
|
|
97
140
|
): Promise<RegistryInfo[]> {
|
|
141
|
+
console.debug('Loading registries...');
|
|
98
142
|
const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
|
|
99
143
|
loadProxyConfig(proxyConfigPath),
|
|
100
144
|
loadYarnConfig(localYarnConfigPath),
|
|
@@ -113,13 +157,16 @@ async function loadRegistries(
|
|
|
113
157
|
config.npmRegistries?.[url];
|
|
114
158
|
if (registryConfig?.npmAuthToken) {
|
|
115
159
|
token = registryConfig.npmAuthToken;
|
|
160
|
+
console.debug(`Found token for ${normalizedUrl} in Yarn config`);
|
|
116
161
|
break;
|
|
117
162
|
}
|
|
118
163
|
}
|
|
119
164
|
}
|
|
120
165
|
registryMap.set(normalizedUrl, { url: normalizedUrl, token });
|
|
121
166
|
}
|
|
122
|
-
|
|
167
|
+
const registries = Array.from(registryMap.values());
|
|
168
|
+
console.log('Loaded registries:', registries);
|
|
169
|
+
return registries;
|
|
123
170
|
}
|
|
124
171
|
|
|
125
172
|
export async function startProxyServer(
|
|
@@ -139,13 +186,17 @@ export async function startProxyServer(
|
|
|
139
186
|
let proxyPort: number;
|
|
140
187
|
|
|
141
188
|
const requestHandler = async (req: any, res: any) => {
|
|
189
|
+
console.debug(`Received request: ${req.method} ${req.url}`);
|
|
142
190
|
if (!req.url || !req.headers.host) {
|
|
191
|
+
console.error('Invalid request: missing URL or host header');
|
|
143
192
|
res.writeHead(400).end('Invalid Request');
|
|
144
193
|
return;
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
197
|
+
console.debug(`Full URL: ${fullUrl.toString()}`);
|
|
148
198
|
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
199
|
+
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePath}`);
|
|
149
200
|
res.writeHead(404).end('Not Found');
|
|
150
201
|
return;
|
|
151
202
|
}
|
|
@@ -155,23 +206,26 @@ export async function startProxyServer(
|
|
|
155
206
|
: fullUrl.pathname;
|
|
156
207
|
console.log(`Proxying: ${relativePath}`);
|
|
157
208
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
209
|
+
const fetchPromises = registries.map(async ({ url, token }) => {
|
|
210
|
+
await limiter.acquire(); // 获取并发许可
|
|
211
|
+
try {
|
|
212
|
+
const cleanRelativePath = relativePath.replace(/\/+$/, '');
|
|
213
|
+
const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
|
|
214
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
215
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
216
|
+
const response = await fetch(targetUrl, { headers });
|
|
217
|
+
console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
|
|
218
|
+
console.debug(`Response headers from ${url}:`, Object.fromEntries(response.headers.entries()));
|
|
219
|
+
return response.ok ? response : null;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error(`Failed to fetch from ${url}:`, e);
|
|
222
|
+
return null;
|
|
223
|
+
} finally {
|
|
224
|
+
limiter.release(); // 释放并发许可
|
|
225
|
+
}
|
|
226
|
+
});
|
|
174
227
|
|
|
228
|
+
const responses = await Promise.all(fetchPromises);
|
|
175
229
|
const successResponse = responses.find((r): r is Response => r !== null);
|
|
176
230
|
if (!successResponse) {
|
|
177
231
|
console.error(`All registries failed for ${relativePath}`);
|
|
@@ -180,21 +234,28 @@ export async function startProxyServer(
|
|
|
180
234
|
}
|
|
181
235
|
|
|
182
236
|
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
237
|
+
console.debug(`Content-Type: ${contentType}`);
|
|
183
238
|
if (contentType.includes('application/json')) {
|
|
184
239
|
try {
|
|
185
240
|
const data = await successResponse.json() as PackageData;
|
|
241
|
+
console.debug('JSON response data:', JSON.stringify(data, null, 2));
|
|
186
242
|
if (data.versions) {
|
|
187
243
|
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
244
|
+
console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
|
|
188
245
|
for (const version in data.versions) {
|
|
189
246
|
const dist = data.versions[version]?.dist;
|
|
190
247
|
if (dist?.tarball) {
|
|
191
248
|
const originalUrl = new URL(dist.tarball);
|
|
192
249
|
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
193
250
|
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
251
|
+
console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
|
|
194
252
|
}
|
|
195
253
|
}
|
|
196
254
|
}
|
|
197
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
255
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
256
|
+
const jsonResponse = JSON.stringify(data);
|
|
257
|
+
console.debug(`Sending JSON response: ${jsonResponse}`);
|
|
258
|
+
res.end(jsonResponse);
|
|
198
259
|
} catch (e) {
|
|
199
260
|
console.error('Failed to parse JSON response:', e);
|
|
200
261
|
res.writeHead(502).end('Invalid Upstream Response');
|
|
@@ -209,24 +270,31 @@ export async function startProxyServer(
|
|
|
209
270
|
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
210
271
|
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
211
272
|
};
|
|
273
|
+
console.debug(`Streaming response with headers:`, safeHeaders);
|
|
212
274
|
res.writeHead(successResponse.status, safeHeaders);
|
|
213
|
-
successResponse.body.pipe(res)
|
|
275
|
+
successResponse.body.pipe(res).on('error', (err:any) => {
|
|
276
|
+
console.error(`Stream error for ${relativePath}:`, err);
|
|
277
|
+
res.writeHead(502).end('Stream Error');
|
|
278
|
+
});
|
|
214
279
|
}
|
|
215
280
|
};
|
|
216
281
|
|
|
217
282
|
let server: HttpServer | HttpsServer;
|
|
218
283
|
if (proxyConfig.https) {
|
|
219
284
|
const { key, cert } = proxyConfig.https;
|
|
285
|
+
const keyPath = resolvePath(key);
|
|
286
|
+
const certPath = resolvePath(cert);
|
|
287
|
+
console.debug(`Loading HTTPS key: ${keyPath}, cert: ${certPath}`);
|
|
220
288
|
try {
|
|
221
|
-
await fsPromises.access(
|
|
222
|
-
await fsPromises.access(
|
|
289
|
+
await fsPromises.access(keyPath);
|
|
290
|
+
await fsPromises.access(certPath);
|
|
223
291
|
} catch (e) {
|
|
224
292
|
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
225
293
|
process.exit(1);
|
|
226
294
|
}
|
|
227
295
|
const httpsOptions = {
|
|
228
|
-
key: readFileSync(
|
|
229
|
-
cert: readFileSync(
|
|
296
|
+
key: readFileSync(keyPath),
|
|
297
|
+
cert: readFileSync(certPath),
|
|
230
298
|
};
|
|
231
299
|
server = createHttpsServer(httpsOptions, requestHandler);
|
|
232
300
|
} else {
|
|
@@ -239,12 +307,14 @@ export async function startProxyServer(
|
|
|
239
307
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
240
308
|
process.exit(1);
|
|
241
309
|
}
|
|
310
|
+
console.error('Server error:', err);
|
|
242
311
|
reject(err);
|
|
243
312
|
});
|
|
244
313
|
server.listen(port, () => {
|
|
245
314
|
const address = server.address() as AddressInfo;
|
|
246
315
|
proxyPort = address.port;
|
|
247
316
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
317
|
+
console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
|
|
248
318
|
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
249
319
|
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
250
320
|
resolve(server);
|
|
@@ -254,6 +324,7 @@ export async function startProxyServer(
|
|
|
254
324
|
|
|
255
325
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
256
326
|
const [,, configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
327
|
+
console.debug(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
|
|
257
328
|
startProxyServer(
|
|
258
329
|
configPath,
|
|
259
330
|
localYarnPath,
|