com.jimuwd.xian.registry-proxy 1.0.12 → 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 +91 -17
- package/package.json +1 -1
- package/src/index.ts +104 -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,13 +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
|
-
// Handle base path
|
|
114
159
|
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
160
|
+
console.debug(`Full URL: ${fullUrl.toString()}`);
|
|
115
161
|
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
162
|
+
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePath}`);
|
|
116
163
|
res.writeHead(404).end('Not Found');
|
|
117
164
|
return;
|
|
118
165
|
}
|
|
@@ -120,39 +167,56 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
120
167
|
? fullUrl.pathname.slice(basePath.length)
|
|
121
168
|
: fullUrl.pathname;
|
|
122
169
|
console.log(`Proxying: ${relativePath}`);
|
|
123
|
-
const
|
|
170
|
+
const fetchPromises = registries.map(async ({ url, token }) => {
|
|
171
|
+
await limiter.acquire(); // 获取并发许可
|
|
124
172
|
try {
|
|
125
|
-
const
|
|
173
|
+
const cleanRelativePath = relativePath.replace(/\/+$/, '');
|
|
174
|
+
const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
|
|
175
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
126
176
|
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
127
177
|
const response = await fetch(targetUrl, { headers });
|
|
178
|
+
console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
|
|
179
|
+
console.debug(`Response headers from ${url}:`, Object.fromEntries(response.headers.entries()));
|
|
128
180
|
return response.ok ? response : null;
|
|
129
181
|
}
|
|
130
182
|
catch (e) {
|
|
131
183
|
console.error(`Failed to fetch from ${url}:`, e);
|
|
132
184
|
return null;
|
|
133
185
|
}
|
|
134
|
-
|
|
186
|
+
finally {
|
|
187
|
+
limiter.release(); // 释放并发许可
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const responses = await Promise.all(fetchPromises);
|
|
135
191
|
const successResponse = responses.find((r) => r !== null);
|
|
136
192
|
if (!successResponse) {
|
|
137
|
-
|
|
193
|
+
console.error(`All registries failed for ${relativePath}`);
|
|
194
|
+
res.writeHead(404).end('Not Found - All upstream registries failed');
|
|
138
195
|
return;
|
|
139
196
|
}
|
|
140
197
|
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
198
|
+
console.debug(`Content-Type: ${contentType}`);
|
|
141
199
|
if (contentType.includes('application/json')) {
|
|
142
200
|
try {
|
|
143
201
|
const data = await successResponse.json();
|
|
202
|
+
console.debug('JSON response data:', JSON.stringify(data, null, 2));
|
|
144
203
|
if (data.versions) {
|
|
145
204
|
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
205
|
+
console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
|
|
146
206
|
for (const version in data.versions) {
|
|
147
207
|
const dist = data.versions[version]?.dist;
|
|
148
208
|
if (dist?.tarball) {
|
|
149
209
|
const originalUrl = new URL(dist.tarball);
|
|
150
210
|
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
151
211
|
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
212
|
+
console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
|
|
152
213
|
}
|
|
153
214
|
}
|
|
154
215
|
}
|
|
155
|
-
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);
|
|
156
220
|
}
|
|
157
221
|
catch (e) {
|
|
158
222
|
console.error('Failed to parse JSON response:', e);
|
|
@@ -169,24 +233,31 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
169
233
|
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
170
234
|
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
171
235
|
};
|
|
236
|
+
console.debug(`Streaming response with headers:`, safeHeaders);
|
|
172
237
|
res.writeHead(successResponse.status, safeHeaders);
|
|
173
|
-
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
|
+
});
|
|
174
242
|
}
|
|
175
243
|
};
|
|
176
244
|
let server;
|
|
177
245
|
if (proxyConfig.https) {
|
|
178
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}`);
|
|
179
250
|
try {
|
|
180
|
-
await fsPromises.access(
|
|
181
|
-
await fsPromises.access(
|
|
251
|
+
await fsPromises.access(keyPath);
|
|
252
|
+
await fsPromises.access(certPath);
|
|
182
253
|
}
|
|
183
254
|
catch (e) {
|
|
184
255
|
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
185
256
|
process.exit(1);
|
|
186
257
|
}
|
|
187
258
|
const httpsOptions = {
|
|
188
|
-
key: readFileSync(
|
|
189
|
-
cert: readFileSync(
|
|
259
|
+
key: readFileSync(keyPath),
|
|
260
|
+
cert: readFileSync(certPath),
|
|
190
261
|
};
|
|
191
262
|
server = createHttpsServer(httpsOptions, requestHandler);
|
|
192
263
|
}
|
|
@@ -199,12 +270,14 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
199
270
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
200
271
|
process.exit(1);
|
|
201
272
|
}
|
|
273
|
+
console.error('Server error:', err);
|
|
202
274
|
reject(err);
|
|
203
275
|
});
|
|
204
276
|
server.listen(port, () => {
|
|
205
277
|
const address = server.address();
|
|
206
278
|
proxyPort = address.port;
|
|
207
279
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
280
|
+
console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
|
|
208
281
|
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
209
282
|
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
210
283
|
resolve(server);
|
|
@@ -213,6 +286,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
213
286
|
}
|
|
214
287
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
215
288
|
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
289
|
+
console.debug(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
|
|
216
290
|
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
217
291
|
console.error('Failed to start server:', err);
|
|
218
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,14 +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
|
-
// Handle base path
|
|
148
196
|
const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
197
|
+
console.debug(`Full URL: ${fullUrl.toString()}`);
|
|
149
198
|
if (basePath && !fullUrl.pathname.startsWith(basePath)) {
|
|
199
|
+
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePath}`);
|
|
150
200
|
res.writeHead(404).end('Not Found');
|
|
151
201
|
return;
|
|
152
202
|
}
|
|
@@ -154,45 +204,58 @@ export async function startProxyServer(
|
|
|
154
204
|
const relativePath = basePath
|
|
155
205
|
? fullUrl.pathname.slice(basePath.length)
|
|
156
206
|
: fullUrl.pathname;
|
|
157
|
-
|
|
158
207
|
console.log(`Proxying: ${relativePath}`);
|
|
159
208
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
})
|
|
172
|
-
|
|
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
|
+
});
|
|
173
227
|
|
|
228
|
+
const responses = await Promise.all(fetchPromises);
|
|
174
229
|
const successResponse = responses.find((r): r is Response => r !== null);
|
|
175
230
|
if (!successResponse) {
|
|
176
|
-
|
|
231
|
+
console.error(`All registries failed for ${relativePath}`);
|
|
232
|
+
res.writeHead(404).end('Not Found - All upstream registries failed');
|
|
177
233
|
return;
|
|
178
234
|
}
|
|
179
235
|
|
|
180
236
|
const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
|
|
237
|
+
console.debug(`Content-Type: ${contentType}`);
|
|
181
238
|
if (contentType.includes('application/json')) {
|
|
182
239
|
try {
|
|
183
240
|
const data = await successResponse.json() as PackageData;
|
|
241
|
+
console.debug('JSON response data:', JSON.stringify(data, null, 2));
|
|
184
242
|
if (data.versions) {
|
|
185
243
|
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
244
|
+
console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
|
|
186
245
|
for (const version in data.versions) {
|
|
187
246
|
const dist = data.versions[version]?.dist;
|
|
188
247
|
if (dist?.tarball) {
|
|
189
248
|
const originalUrl = new URL(dist.tarball);
|
|
190
249
|
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
191
250
|
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
251
|
+
console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
|
|
192
252
|
}
|
|
193
253
|
}
|
|
194
254
|
}
|
|
195
|
-
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);
|
|
196
259
|
} catch (e) {
|
|
197
260
|
console.error('Failed to parse JSON response:', e);
|
|
198
261
|
res.writeHead(502).end('Invalid Upstream Response');
|
|
@@ -207,24 +270,31 @@ export async function startProxyServer(
|
|
|
207
270
|
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
208
271
|
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
209
272
|
};
|
|
273
|
+
console.debug(`Streaming response with headers:`, safeHeaders);
|
|
210
274
|
res.writeHead(successResponse.status, safeHeaders);
|
|
211
|
-
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
|
+
});
|
|
212
279
|
}
|
|
213
280
|
};
|
|
214
281
|
|
|
215
282
|
let server: HttpServer | HttpsServer;
|
|
216
283
|
if (proxyConfig.https) {
|
|
217
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}`);
|
|
218
288
|
try {
|
|
219
|
-
await fsPromises.access(
|
|
220
|
-
await fsPromises.access(
|
|
289
|
+
await fsPromises.access(keyPath);
|
|
290
|
+
await fsPromises.access(certPath);
|
|
221
291
|
} catch (e) {
|
|
222
292
|
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
223
293
|
process.exit(1);
|
|
224
294
|
}
|
|
225
295
|
const httpsOptions = {
|
|
226
|
-
key: readFileSync(
|
|
227
|
-
cert: readFileSync(
|
|
296
|
+
key: readFileSync(keyPath),
|
|
297
|
+
cert: readFileSync(certPath),
|
|
228
298
|
};
|
|
229
299
|
server = createHttpsServer(httpsOptions, requestHandler);
|
|
230
300
|
} else {
|
|
@@ -237,12 +307,14 @@ export async function startProxyServer(
|
|
|
237
307
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
238
308
|
process.exit(1);
|
|
239
309
|
}
|
|
310
|
+
console.error('Server error:', err);
|
|
240
311
|
reject(err);
|
|
241
312
|
});
|
|
242
313
|
server.listen(port, () => {
|
|
243
314
|
const address = server.address() as AddressInfo;
|
|
244
315
|
proxyPort = address.port;
|
|
245
316
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
317
|
+
console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
|
|
246
318
|
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
247
319
|
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
248
320
|
resolve(server);
|
|
@@ -252,6 +324,7 @@ export async function startProxyServer(
|
|
|
252
324
|
|
|
253
325
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
254
326
|
const [,, configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
327
|
+
console.debug(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
|
|
255
328
|
startProxyServer(
|
|
256
329
|
configPath,
|
|
257
330
|
localYarnPath,
|