com.jimuwd.xian.registry-proxy 1.0.34 → 1.0.36
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 +113 -98
- package/package.json +1 -1
- package/src/index.ts +137 -100
package/dist/index.js
CHANGED
|
@@ -139,6 +139,102 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
|
|
|
139
139
|
const basePath = removeEndingSlashAndForceStartingSlash(proxyConfig.basePath);
|
|
140
140
|
return { registries, https, basePath };
|
|
141
141
|
}
|
|
142
|
+
async function fetchFromRegistry(registry, path, search, limiter) {
|
|
143
|
+
await limiter.acquire();
|
|
144
|
+
try {
|
|
145
|
+
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
146
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
147
|
+
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
148
|
+
const response = await fetch(targetUrl, { headers });
|
|
149
|
+
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
150
|
+
return response.ok ? response : null;
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (e instanceof Error) {
|
|
154
|
+
console.error(e.code === 'ECONNREFUSED'
|
|
155
|
+
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
156
|
+
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
limiter.release();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// 修改后的 writeResponse 函数
|
|
165
|
+
async function writeResponse(res, response, req, proxyInfo, proxyPort, registryInfos) {
|
|
166
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
167
|
+
// 准备通用头信息
|
|
168
|
+
const safeHeaders = {
|
|
169
|
+
'content-type': contentType,
|
|
170
|
+
'connection': 'keep-alive'
|
|
171
|
+
};
|
|
172
|
+
// 复制所有可能需要的头信息
|
|
173
|
+
const headersToCopy = [
|
|
174
|
+
'cache-control', 'etag', 'last-modified',
|
|
175
|
+
'content-encoding', 'content-length'
|
|
176
|
+
];
|
|
177
|
+
headersToCopy.forEach(header => {
|
|
178
|
+
const value = response.headers.get(header);
|
|
179
|
+
if (value)
|
|
180
|
+
safeHeaders[header] = value;
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
if (contentType.includes('application/json')) {
|
|
184
|
+
// JSON 处理逻辑
|
|
185
|
+
const data = await response.json();
|
|
186
|
+
if (data.versions) {
|
|
187
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
188
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
189
|
+
for (const version in data.versions) {
|
|
190
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
191
|
+
if (tarball) {
|
|
192
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
193
|
+
data.versions[version].dist.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
res.writeHead(200, safeHeaders);
|
|
198
|
+
res.end(JSON.stringify(data));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// 二进制流处理
|
|
202
|
+
if (!response.body) {
|
|
203
|
+
console.error('Empty response body');
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
// 修复流类型转换问题
|
|
207
|
+
let nodeStream;
|
|
208
|
+
if (typeof Readable.fromWeb === 'function') {
|
|
209
|
+
// Node.js 17+ 标准方式
|
|
210
|
+
nodeStream = Readable.fromWeb(response.body);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Node.js 16 及以下版本的兼容方案
|
|
214
|
+
const { PassThrough } = await import('stream');
|
|
215
|
+
const passThrough = new PassThrough();
|
|
216
|
+
const reader = response.body.getReader();
|
|
217
|
+
const pump = async () => {
|
|
218
|
+
const { done, value } = await reader.read();
|
|
219
|
+
if (done)
|
|
220
|
+
return passThrough.end();
|
|
221
|
+
passThrough.write(value);
|
|
222
|
+
await pump();
|
|
223
|
+
};
|
|
224
|
+
pump().catch(err => passThrough.destroy(err));
|
|
225
|
+
nodeStream = passThrough;
|
|
226
|
+
}
|
|
227
|
+
res.writeHead(response.status, safeHeaders);
|
|
228
|
+
await pipeline(nodeStream, res);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
console.error('Failed to write response:', err);
|
|
233
|
+
if (!res.headersSent) {
|
|
234
|
+
res.writeHead(502).end('Internal Server Error');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
142
238
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
143
239
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
144
240
|
const registryInfos = proxyInfo.registries;
|
|
@@ -149,116 +245,35 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
149
245
|
let proxyPort;
|
|
150
246
|
const requestHandler = async (req, res) => {
|
|
151
247
|
if (!req.url || !req.headers.host) {
|
|
152
|
-
console.error('Invalid request: missing URL or host header');
|
|
153
248
|
res.writeHead(400).end('Invalid Request');
|
|
154
249
|
return;
|
|
155
250
|
}
|
|
156
251
|
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
157
|
-
console.log(`Proxy server received request on ${fullUrl.toString()}`);
|
|
158
252
|
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
159
|
-
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
|
|
160
253
|
res.writeHead(404).end('Not Found');
|
|
161
254
|
return;
|
|
162
255
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
256
|
+
const path = basePathPrefixedWithSlash === '/'
|
|
257
|
+
? fullUrl.pathname
|
|
258
|
+
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
259
|
+
// 顺序尝试注册表,获取第一个成功响应
|
|
260
|
+
let successfulResponse = null;
|
|
261
|
+
for (const registry of registryInfos) {
|
|
167
262
|
if (req.destroyed)
|
|
168
263
|
break;
|
|
169
|
-
await limiter
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log(`Fetching from: ${targetUrl}`);
|
|
174
|
-
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
175
|
-
response = await fetch(targetUrl, { headers });
|
|
176
|
-
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
177
|
-
if (response.ok) {
|
|
178
|
-
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
179
|
-
if (contentType.includes('application/json')) {
|
|
180
|
-
// application/json 元数据
|
|
181
|
-
try {
|
|
182
|
-
const data = await response.json();
|
|
183
|
-
if (data.versions) {
|
|
184
|
-
const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
|
|
185
|
-
console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
|
|
186
|
-
const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
|
|
187
|
-
console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
|
|
188
|
-
for (const version in data.versions) {
|
|
189
|
-
const dist = data.versions[version]?.dist;
|
|
190
|
-
if (dist?.tarball) {
|
|
191
|
-
const originalUrl = new URL(dist.tarball);
|
|
192
|
-
const originalSearchParamsStr = originalUrl.search || '';
|
|
193
|
-
const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
|
|
194
|
-
dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
|
|
195
|
-
if (!tarballPathPrefixedWithSlash.startsWith("/"))
|
|
196
|
-
console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
201
|
-
res.end(JSON.stringify(data));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
catch (e) {
|
|
205
|
-
console.error('Failed to parse JSON response:', e);
|
|
206
|
-
// 继续尝试下一个注册表
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
// 二进制流处理
|
|
211
|
-
if (!response.body) {
|
|
212
|
-
console.error(`Empty response body from ${response.url}`);
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
// 头信息处理
|
|
216
|
-
const safeHeaders = {
|
|
217
|
-
'content-type': contentType,
|
|
218
|
-
'connection': 'keep-alive',
|
|
219
|
-
};
|
|
220
|
-
// 复制关键头信息
|
|
221
|
-
['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
|
|
222
|
-
const value = response?.headers.get(header);
|
|
223
|
-
if (value)
|
|
224
|
-
safeHeaders[header] = value;
|
|
225
|
-
});
|
|
226
|
-
res.writeHead(response.status, safeHeaders);
|
|
227
|
-
// 流转换与传输
|
|
228
|
-
const nodeStream = Readable.fromWeb(response.body);
|
|
229
|
-
let isComplete = false;
|
|
230
|
-
const cleanUp = () => {
|
|
231
|
-
if (!isComplete)
|
|
232
|
-
nodeStream.destroy();
|
|
233
|
-
};
|
|
234
|
-
try {
|
|
235
|
-
req.on('close', cleanUp);
|
|
236
|
-
res.on('close', cleanUp);
|
|
237
|
-
await pipeline(nodeStream, res);
|
|
238
|
-
isComplete = true;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
finally {
|
|
242
|
-
req.off('close', cleanUp);
|
|
243
|
-
res.off('close', cleanUp);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
catch (e) {
|
|
249
|
-
// 增强错误日志
|
|
250
|
-
if (e instanceof Error) {
|
|
251
|
-
console.error(e.code === 'ECONNREFUSED'
|
|
252
|
-
? `Registry ${normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
253
|
-
: `Error from ${normalizedRegistryUrl}: ${e.message}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
finally {
|
|
257
|
-
limiter.release();
|
|
264
|
+
const response = await fetchFromRegistry(registry, path, fullUrl.search, limiter);
|
|
265
|
+
if (response) {
|
|
266
|
+
successfulResponse = response;
|
|
267
|
+
break;
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
|
-
//
|
|
261
|
-
|
|
270
|
+
// 统一回写响应
|
|
271
|
+
if (successfulResponse) {
|
|
272
|
+
await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
res.writeHead(404).end('All upstream registries failed');
|
|
276
|
+
}
|
|
262
277
|
};
|
|
263
278
|
let server;
|
|
264
279
|
if (proxyInfo.https) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ interface PackageData {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
class ConcurrencyLimiter {
|
|
55
|
-
private maxConcurrency: number;
|
|
55
|
+
private readonly maxConcurrency: number;
|
|
56
56
|
private current: number = 0;
|
|
57
57
|
private queue: Array<() => void> = [];
|
|
58
58
|
|
|
@@ -187,6 +187,120 @@ async function loadProxyInfo(
|
|
|
187
187
|
return {registries, https, basePath};
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
async function fetchFromRegistry(
|
|
191
|
+
registry: RegistryInfo,
|
|
192
|
+
path: string,
|
|
193
|
+
search: string,
|
|
194
|
+
limiter: ConcurrencyLimiter
|
|
195
|
+
): Promise<Response | null> {
|
|
196
|
+
await limiter.acquire();
|
|
197
|
+
try {
|
|
198
|
+
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
199
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
200
|
+
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
201
|
+
const response = await fetch(targetUrl, { headers });
|
|
202
|
+
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
203
|
+
return response.ok ? response : null;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (e instanceof Error) {
|
|
206
|
+
console.error(
|
|
207
|
+
(e as any).code === 'ECONNREFUSED'
|
|
208
|
+
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
209
|
+
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
} finally {
|
|
214
|
+
limiter.release();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 修改后的 writeResponse 函数
|
|
219
|
+
async function writeResponse(
|
|
220
|
+
res: ServerResponse,
|
|
221
|
+
response: Response,
|
|
222
|
+
req: IncomingMessage,
|
|
223
|
+
proxyInfo: ProxyInfo,
|
|
224
|
+
proxyPort: number,
|
|
225
|
+
registryInfos: RegistryInfo[]
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
228
|
+
|
|
229
|
+
// 准备通用头信息
|
|
230
|
+
const safeHeaders: OutgoingHttpHeaders = {
|
|
231
|
+
'content-type': contentType,
|
|
232
|
+
'connection': 'keep-alive'
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// 复制所有可能需要的头信息
|
|
236
|
+
const headersToCopy = [
|
|
237
|
+
'cache-control', 'etag', 'last-modified',
|
|
238
|
+
'content-encoding', 'content-length'
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
headersToCopy.forEach(header => {
|
|
242
|
+
const value = response.headers.get(header);
|
|
243
|
+
if (value) safeHeaders[header] = value;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (contentType.includes('application/json')) {
|
|
248
|
+
// JSON 处理逻辑
|
|
249
|
+
const data = await response.json() as PackageData;
|
|
250
|
+
if (data.versions) {
|
|
251
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
252
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
253
|
+
|
|
254
|
+
for (const version in data.versions) {
|
|
255
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
256
|
+
if (tarball) {
|
|
257
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
258
|
+
data.versions[version]!.dist!.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
res.writeHead(200, safeHeaders);
|
|
263
|
+
res.end(JSON.stringify(data));
|
|
264
|
+
} else {
|
|
265
|
+
// 二进制流处理
|
|
266
|
+
if (!response.body) {
|
|
267
|
+
console.error('Empty response body');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 修复流类型转换问题
|
|
272
|
+
let nodeStream: NodeJS.ReadableStream;
|
|
273
|
+
if (typeof Readable.fromWeb === 'function') {
|
|
274
|
+
// Node.js 17+ 标准方式
|
|
275
|
+
nodeStream = Readable.fromWeb(response.body as any);
|
|
276
|
+
} else {
|
|
277
|
+
// Node.js 16 及以下版本的兼容方案
|
|
278
|
+
const { PassThrough } = await import('stream');
|
|
279
|
+
const passThrough = new PassThrough();
|
|
280
|
+
const reader = (response.body as any).getReader();
|
|
281
|
+
|
|
282
|
+
const pump = async () => {
|
|
283
|
+
const { done, value } = await reader.read();
|
|
284
|
+
if (done) return passThrough.end();
|
|
285
|
+
passThrough.write(value);
|
|
286
|
+
await pump();
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
pump().catch(err => passThrough.destroy(err));
|
|
290
|
+
nodeStream = passThrough;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
res.writeHead(response.status, safeHeaders);
|
|
294
|
+
await pipeline(nodeStream, res);
|
|
295
|
+
}
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error('Failed to write response:', err);
|
|
298
|
+
if (!res.headersSent) {
|
|
299
|
+
res.writeHead(502).end('Internal Server Error');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
190
304
|
export async function startProxyServer(
|
|
191
305
|
proxyConfigPath?: string,
|
|
192
306
|
localYarnConfigPath?: string,
|
|
@@ -205,121 +319,44 @@ export async function startProxyServer(
|
|
|
205
319
|
|
|
206
320
|
const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
|
|
207
321
|
if (!req.url || !req.headers.host) {
|
|
208
|
-
console.error('Invalid request: missing URL or host header');
|
|
209
322
|
res.writeHead(400).end('Invalid Request');
|
|
210
323
|
return;
|
|
211
324
|
}
|
|
212
325
|
|
|
213
326
|
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
214
|
-
console.log(`Proxy server received request on ${fullUrl.toString()}`)
|
|
215
327
|
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
216
|
-
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
|
|
217
328
|
res.writeHead(404).end('Not Found');
|
|
218
329
|
return;
|
|
219
330
|
}
|
|
220
331
|
|
|
221
|
-
const
|
|
222
|
-
|
|
332
|
+
const path = basePathPrefixedWithSlash === '/'
|
|
333
|
+
? fullUrl.pathname
|
|
334
|
+
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
223
335
|
|
|
224
|
-
//
|
|
225
|
-
|
|
336
|
+
// 顺序尝试注册表,获取第一个成功响应
|
|
337
|
+
let successfulResponse: Response | null = null;
|
|
338
|
+
for (const registry of registryInfos) {
|
|
226
339
|
if (req.destroyed) break;
|
|
227
|
-
await limiter.acquire();
|
|
228
|
-
let response: Response | null = null;
|
|
229
|
-
try {
|
|
230
|
-
const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
|
|
231
|
-
console.log(`Fetching from: ${targetUrl}`);
|
|
232
|
-
const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
|
|
233
|
-
response = await fetch(targetUrl, {headers});
|
|
234
|
-
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
235
|
-
|
|
236
|
-
if (response.ok) {
|
|
237
|
-
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
238
|
-
|
|
239
|
-
if (contentType.includes('application/json')) {
|
|
240
|
-
// application/json 元数据
|
|
241
|
-
try {
|
|
242
|
-
const data = await response.json() as PackageData;
|
|
243
|
-
if (data.versions) {
|
|
244
|
-
const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
|
|
245
|
-
console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
|
|
246
|
-
const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
|
|
247
|
-
console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
|
|
248
|
-
for (const version in data.versions) {
|
|
249
|
-
const dist = data.versions[version]?.dist;
|
|
250
|
-
if (dist?.tarball) {
|
|
251
|
-
const originalUrl = new URL(dist.tarball);
|
|
252
|
-
const originalSearchParamsStr = originalUrl.search || '';
|
|
253
|
-
const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
|
|
254
|
-
dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
|
|
255
|
-
if (!tarballPathPrefixedWithSlash.startsWith("/")) console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
260
|
-
res.end(JSON.stringify(data));
|
|
261
|
-
return;
|
|
262
|
-
} catch (e) {
|
|
263
|
-
console.error('Failed to parse JSON response:', e);
|
|
264
|
-
// 继续尝试下一个注册表
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
// 二进制流处理
|
|
268
|
-
if (!response.body) {
|
|
269
|
-
console.error(`Empty response body from ${response.url}`);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
340
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
res.writeHead(response.status, safeHeaders);
|
|
286
|
-
|
|
287
|
-
// 流转换与传输
|
|
288
|
-
const nodeStream = Readable.fromWeb(response.body as any);
|
|
289
|
-
let isComplete = false;
|
|
290
|
-
const cleanUp = () => {
|
|
291
|
-
if (!isComplete) nodeStream.destroy();
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
req.on('close', cleanUp);
|
|
297
|
-
res.on('close', cleanUp);
|
|
298
|
-
await pipeline(nodeStream, res);
|
|
299
|
-
isComplete = true;
|
|
300
|
-
return;
|
|
301
|
-
} finally {
|
|
302
|
-
req.off('close', cleanUp);
|
|
303
|
-
res.off('close', cleanUp);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} catch (e) {
|
|
308
|
-
// 增强错误日志
|
|
309
|
-
if (e instanceof Error) {
|
|
310
|
-
console.error(
|
|
311
|
-
(e as any).code === 'ECONNREFUSED'
|
|
312
|
-
? `Registry ${normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
313
|
-
: `Error from ${normalizedRegistryUrl}: ${e.message}`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
} finally {
|
|
317
|
-
limiter.release();
|
|
341
|
+
const response = await fetchFromRegistry(
|
|
342
|
+
registry,
|
|
343
|
+
path,
|
|
344
|
+
fullUrl.search,
|
|
345
|
+
limiter
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (response) {
|
|
349
|
+
successfulResponse = response;
|
|
350
|
+
break;
|
|
318
351
|
}
|
|
319
352
|
}
|
|
320
353
|
|
|
321
|
-
//
|
|
322
|
-
|
|
354
|
+
// 统一回写响应
|
|
355
|
+
if (successfulResponse) {
|
|
356
|
+
await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
357
|
+
} else {
|
|
358
|
+
res.writeHead(404).end('All upstream registries failed');
|
|
359
|
+
}
|
|
323
360
|
};
|
|
324
361
|
|
|
325
362
|
let server: HttpServer | HttpsServer;
|