com.jimuwd.xian.registry-proxy 1.0.13 → 1.0.15
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 +83 -14
- package/package.json +1 -1
- package/src/index.ts +100 -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,26 @@ 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();
|
|
147
202
|
if (data.versions) {
|
|
148
203
|
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
204
|
+
console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
|
|
149
205
|
for (const version in data.versions) {
|
|
150
206
|
const dist = data.versions[version]?.dist;
|
|
151
207
|
if (dist?.tarball) {
|
|
152
208
|
const originalUrl = new URL(dist.tarball);
|
|
153
209
|
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
154
210
|
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
211
|
+
console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
|
|
155
212
|
}
|
|
156
213
|
}
|
|
157
214
|
}
|
|
158
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
215
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
216
|
+
const jsonResponse = JSON.stringify(data);
|
|
217
|
+
res.end(jsonResponse);
|
|
159
218
|
}
|
|
160
219
|
catch (e) {
|
|
161
220
|
console.error('Failed to parse JSON response:', e);
|
|
@@ -172,24 +231,31 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
172
231
|
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
173
232
|
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
174
233
|
};
|
|
234
|
+
console.debug(`Streaming response with headers:`, safeHeaders);
|
|
175
235
|
res.writeHead(successResponse.status, safeHeaders);
|
|
176
|
-
successResponse.body.pipe(res)
|
|
236
|
+
successResponse.body.pipe(res).on('error', (err) => {
|
|
237
|
+
console.error(`Stream error for ${relativePath}:`, err);
|
|
238
|
+
res.writeHead(502).end('Stream Error');
|
|
239
|
+
});
|
|
177
240
|
}
|
|
178
241
|
};
|
|
179
242
|
let server;
|
|
180
243
|
if (proxyConfig.https) {
|
|
181
244
|
const { key, cert } = proxyConfig.https;
|
|
245
|
+
const keyPath = resolvePath(key);
|
|
246
|
+
const certPath = resolvePath(cert);
|
|
247
|
+
console.debug(`Loading HTTPS key: ${keyPath}, cert: ${certPath}`);
|
|
182
248
|
try {
|
|
183
|
-
await fsPromises.access(
|
|
184
|
-
await fsPromises.access(
|
|
249
|
+
await fsPromises.access(keyPath);
|
|
250
|
+
await fsPromises.access(certPath);
|
|
185
251
|
}
|
|
186
252
|
catch (e) {
|
|
187
253
|
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
188
254
|
process.exit(1);
|
|
189
255
|
}
|
|
190
256
|
const httpsOptions = {
|
|
191
|
-
key: readFileSync(
|
|
192
|
-
cert: readFileSync(
|
|
257
|
+
key: readFileSync(keyPath),
|
|
258
|
+
cert: readFileSync(certPath),
|
|
193
259
|
};
|
|
194
260
|
server = createHttpsServer(httpsOptions, requestHandler);
|
|
195
261
|
}
|
|
@@ -202,12 +268,14 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
202
268
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
203
269
|
process.exit(1);
|
|
204
270
|
}
|
|
271
|
+
console.error('Server error:', err);
|
|
205
272
|
reject(err);
|
|
206
273
|
});
|
|
207
274
|
server.listen(port, () => {
|
|
208
275
|
const address = server.address();
|
|
209
276
|
proxyPort = address.port;
|
|
210
277
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
278
|
+
console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
|
|
211
279
|
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
212
280
|
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
213
281
|
resolve(server);
|
|
@@ -216,6 +284,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
216
284
|
}
|
|
217
285
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
218
286
|
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
287
|
+
console.log(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
|
|
219
288
|
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
220
289
|
console.error('Failed to start server:', err);
|
|
221
290
|
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,26 @@ 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;
|
|
186
241
|
if (data.versions) {
|
|
187
242
|
const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
|
|
243
|
+
console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
|
|
188
244
|
for (const version in data.versions) {
|
|
189
245
|
const dist = data.versions[version]?.dist;
|
|
190
246
|
if (dist?.tarball) {
|
|
191
247
|
const originalUrl = new URL(dist.tarball);
|
|
192
248
|
const tarballPath = removeRegistryPrefix(dist.tarball, registries);
|
|
193
249
|
dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
|
|
250
|
+
console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
|
|
194
251
|
}
|
|
195
252
|
}
|
|
196
253
|
}
|
|
197
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
254
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
255
|
+
const jsonResponse = JSON.stringify(data);
|
|
256
|
+
res.end(jsonResponse);
|
|
198
257
|
} catch (e) {
|
|
199
258
|
console.error('Failed to parse JSON response:', e);
|
|
200
259
|
res.writeHead(502).end('Invalid Upstream Response');
|
|
@@ -209,24 +268,31 @@ export async function startProxyServer(
|
|
|
209
268
|
'Content-Type': successResponse.headers.get('Content-Type'),
|
|
210
269
|
'Content-Length': successResponse.headers.get('Content-Length'),
|
|
211
270
|
};
|
|
271
|
+
console.debug(`Streaming response with headers:`, safeHeaders);
|
|
212
272
|
res.writeHead(successResponse.status, safeHeaders);
|
|
213
|
-
successResponse.body.pipe(res)
|
|
273
|
+
successResponse.body.pipe(res).on('error', (err:any) => {
|
|
274
|
+
console.error(`Stream error for ${relativePath}:`, err);
|
|
275
|
+
res.writeHead(502).end('Stream Error');
|
|
276
|
+
});
|
|
214
277
|
}
|
|
215
278
|
};
|
|
216
279
|
|
|
217
280
|
let server: HttpServer | HttpsServer;
|
|
218
281
|
if (proxyConfig.https) {
|
|
219
282
|
const { key, cert } = proxyConfig.https;
|
|
283
|
+
const keyPath = resolvePath(key);
|
|
284
|
+
const certPath = resolvePath(cert);
|
|
285
|
+
console.debug(`Loading HTTPS key: ${keyPath}, cert: ${certPath}`);
|
|
220
286
|
try {
|
|
221
|
-
await fsPromises.access(
|
|
222
|
-
await fsPromises.access(
|
|
287
|
+
await fsPromises.access(keyPath);
|
|
288
|
+
await fsPromises.access(certPath);
|
|
223
289
|
} catch (e) {
|
|
224
290
|
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
225
291
|
process.exit(1);
|
|
226
292
|
}
|
|
227
293
|
const httpsOptions = {
|
|
228
|
-
key: readFileSync(
|
|
229
|
-
cert: readFileSync(
|
|
294
|
+
key: readFileSync(keyPath),
|
|
295
|
+
cert: readFileSync(certPath),
|
|
230
296
|
};
|
|
231
297
|
server = createHttpsServer(httpsOptions, requestHandler);
|
|
232
298
|
} else {
|
|
@@ -239,12 +305,14 @@ export async function startProxyServer(
|
|
|
239
305
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
240
306
|
process.exit(1);
|
|
241
307
|
}
|
|
308
|
+
console.error('Server error:', err);
|
|
242
309
|
reject(err);
|
|
243
310
|
});
|
|
244
311
|
server.listen(port, () => {
|
|
245
312
|
const address = server.address() as AddressInfo;
|
|
246
313
|
proxyPort = address.port;
|
|
247
314
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
315
|
+
console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
|
|
248
316
|
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
249
317
|
console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
|
|
250
318
|
resolve(server);
|
|
@@ -254,6 +322,7 @@ export async function startProxyServer(
|
|
|
254
322
|
|
|
255
323
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
256
324
|
const [,, configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
325
|
+
console.log(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
|
|
257
326
|
startProxyServer(
|
|
258
327
|
configPath,
|
|
259
328
|
localYarnPath,
|