com.jimuwd.xian.registry-proxy 1.0.35 → 1.0.37

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.
Files changed (3) hide show
  1. package/dist/index.js +73 -26
  2. package/package.json +1 -1
  3. package/src/index.ts +82 -41
package/dist/index.js CHANGED
@@ -139,10 +139,9 @@ 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) {
142
+ async function fetchFromRegistry(registry, targetUrl, limiter) {
143
143
  await limiter.acquire();
144
144
  try {
145
- const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
146
145
  console.log(`Fetching from: ${targetUrl}`);
147
146
  const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
148
147
  const response = await fetch(targetUrl, { headers });
@@ -161,36 +160,79 @@ async function fetchFromRegistry(registry, path, search, limiter) {
161
160
  limiter.release();
162
161
  }
163
162
  }
164
- async function writeResponse(res, response, req, proxyInfo, proxyPort, registryInfos) {
165
- const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
166
- if (contentType.includes('application/json')) {
167
- const data = await response.json();
168
- if (data.versions) {
169
- const host = req.headers.host || `localhost:${proxyPort}`;
170
- const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
171
- for (const version in data.versions) {
172
- const tarball = data.versions[version]?.dist?.tarball;
173
- if (tarball) {
174
- const path = removeRegistryPrefix(tarball, registryInfos);
175
- data.versions[version].dist.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
176
- }
177
- }
178
- }
179
- res.writeHead(200, { 'Content-Type': 'application/json' });
180
- res.end(JSON.stringify(data));
181
- }
182
- else {
163
+ // 修改后的 writeSuccessfulResponse 函数
164
+ async function writeSuccessfulResponse(registryInfo, targetUrl, res, response, req, proxyInfo, proxyPort, registryInfos) {
165
+ if (!response.ok)
166
+ throw new Error("Only 2xx response is supported");
167
+ try {
168
+ const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
169
+ // 准备通用头信息
183
170
  const safeHeaders = {
184
171
  'content-type': contentType,
185
- 'connection': 'keep-alive',
172
+ 'connection': 'keep-alive'
186
173
  };
187
- ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
174
+ // 复制所有可能需要的头信息
175
+ const headersToCopy = [
176
+ 'cache-control', 'etag', 'last-modified',
177
+ 'content-encoding', 'content-length'
178
+ ];
179
+ headersToCopy.forEach(header => {
188
180
  const value = response.headers.get(header);
189
181
  if (value)
190
182
  safeHeaders[header] = value;
191
183
  });
192
184
  res.writeHead(response.status, safeHeaders);
193
- await pipeline(Readable.fromWeb(response.body), res);
185
+ if (contentType.includes('application/json')) {
186
+ // JSON 处理逻辑
187
+ const data = await response.json();
188
+ if (data.versions) {
189
+ const host = req.headers.host || `localhost:${proxyPort}`;
190
+ const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
191
+ for (const version in data.versions) {
192
+ const tarball = data.versions[version]?.dist?.tarball;
193
+ if (tarball) {
194
+ const path = removeRegistryPrefix(tarball, registryInfos);
195
+ data.versions[version].dist.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
196
+ }
197
+ }
198
+ }
199
+ res.end(JSON.stringify(data));
200
+ }
201
+ else {
202
+ // 二进制流处理
203
+ if (!response.body) {
204
+ console.error(`Empty response body from ${targetUrl}`);
205
+ }
206
+ else {
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
+ await pipeline(nodeStream, res);
228
+ }
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
+ }
194
236
  }
195
237
  }
196
238
  export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
@@ -216,10 +258,15 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
216
258
  : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
217
259
  // 顺序尝试注册表,获取第一个成功响应
218
260
  let successfulResponse = null;
261
+ let targetRegistry = null;
262
+ let targetUrl = null;
219
263
  for (const registry of registryInfos) {
220
264
  if (req.destroyed)
221
265
  break;
222
- const response = await fetchFromRegistry(registry, path, fullUrl.search, limiter);
266
+ targetRegistry = registry;
267
+ const search = fullUrl.search || '';
268
+ targetUrl = `${registry.normalizedRegistryUrl}${path}${search}`;
269
+ const response = await fetchFromRegistry(registry, targetUrl, limiter);
223
270
  if (response) {
224
271
  successfulResponse = response;
225
272
  break;
@@ -227,7 +274,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
227
274
  }
228
275
  // 统一回写响应
229
276
  if (successfulResponse) {
230
- await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
277
+ await writeSuccessfulResponse(targetRegistry, targetUrl, res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
231
278
  }
232
279
  else {
233
280
  res.writeHead(404).end('All upstream registries failed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "type": "module",
5
5
  "description": "A lightweight npm registry proxy with fallback support",
6
6
  "main": "dist/index.js",
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
 
@@ -189,16 +189,14 @@ async function loadProxyInfo(
189
189
 
190
190
  async function fetchFromRegistry(
191
191
  registry: RegistryInfo,
192
- path: string,
193
- search: string,
192
+ targetUrl: string,
194
193
  limiter: ConcurrencyLimiter
195
194
  ): Promise<Response | null> {
196
195
  await limiter.acquire();
197
196
  try {
198
- const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
199
197
  console.log(`Fetching from: ${targetUrl}`);
200
- const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
201
- const response = await fetch(targetUrl, { headers });
198
+ const headers = registry.token ? {Authorization: `Bearer ${registry.token}`} : undefined;
199
+ const response = await fetch(targetUrl, {headers});
202
200
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
203
201
  return response.ok ? response : null;
204
202
  } catch (e) {
@@ -215,7 +213,10 @@ async function fetchFromRegistry(
215
213
  }
216
214
  }
217
215
 
218
- async function writeResponse(
216
+ // 修改后的 writeSuccessfulResponse 函数
217
+ async function writeSuccessfulResponse(
218
+ registryInfo: RegistryInfo,
219
+ targetUrl: string,
219
220
  res: ServerResponse,
220
221
  response: Response,
221
222
  req: IncomingMessage,
@@ -223,40 +224,82 @@ async function writeResponse(
223
224
  proxyPort: number,
224
225
  registryInfos: RegistryInfo[]
225
226
  ): Promise<void> {
226
- const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
227
-
228
- if (contentType.includes('application/json')) {
229
- const data = await response.json() as PackageData;
230
- if (data.versions) {
231
- const host = req.headers.host || `localhost:${proxyPort}`;
232
- const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
233
-
234
- for (const version in data.versions) {
235
- const tarball = data.versions[version]?.dist?.tarball;
236
- if (tarball) {
237
- const path = removeRegistryPrefix(tarball, registryInfos);
238
- data.versions[version]!.dist!.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
239
- }
240
- }
241
- }
242
- res.writeHead(200, { 'Content-Type': 'application/json' });
243
- res.end(JSON.stringify(data));
244
- } else {
227
+
228
+ if (!response.ok) throw new Error("Only 2xx response is supported");
229
+
230
+ try {
231
+ const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
232
+
233
+ // 准备通用头信息
245
234
  const safeHeaders: OutgoingHttpHeaders = {
246
235
  'content-type': contentType,
247
- 'connection': 'keep-alive',
236
+ 'connection': 'keep-alive'
248
237
  };
249
238
 
250
- ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
239
+ // 复制所有可能需要的头信息
240
+ const headersToCopy = [
241
+ 'cache-control', 'etag', 'last-modified',
242
+ 'content-encoding', 'content-length'
243
+ ];
244
+
245
+ headersToCopy.forEach(header => {
251
246
  const value = response.headers.get(header);
252
247
  if (value) safeHeaders[header] = value;
253
248
  });
254
249
 
250
+
255
251
  res.writeHead(response.status, safeHeaders);
256
- await pipeline(
257
- Readable.fromWeb(response.body as any),
258
- res
259
- );
252
+
253
+ if (contentType.includes('application/json')) {
254
+ // JSON 处理逻辑
255
+ const data = await response.json() as PackageData;
256
+ if (data.versions) {
257
+ const host = req.headers.host || `localhost:${proxyPort}`;
258
+ const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
259
+
260
+ for (const version in data.versions) {
261
+ const tarball = data.versions[version]?.dist?.tarball;
262
+ if (tarball) {
263
+ const path = removeRegistryPrefix(tarball, registryInfos);
264
+ data.versions[version]!.dist!.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
265
+ }
266
+ }
267
+ }
268
+ res.end(JSON.stringify(data));
269
+ } else {
270
+ // 二进制流处理
271
+ if (!response.body) {
272
+ console.error(`Empty response body from ${targetUrl}`);
273
+ } else {
274
+ let nodeStream: NodeJS.ReadableStream;
275
+ if (typeof Readable.fromWeb === 'function') {
276
+ // Node.js 17+ 标准方式
277
+ nodeStream = Readable.fromWeb(response.body as any);
278
+ } else {
279
+ // Node.js 16 及以下版本的兼容方案
280
+ const {PassThrough} = await import('stream');
281
+ const passThrough = new PassThrough();
282
+ const reader = (response.body as any).getReader();
283
+
284
+ const pump = async () => {
285
+ const {done, value} = await reader.read();
286
+ if (done) return passThrough.end();
287
+ passThrough.write(value);
288
+ await pump();
289
+ };
290
+
291
+ pump().catch(err => passThrough.destroy(err));
292
+ nodeStream = passThrough;
293
+ }
294
+
295
+ await pipeline(nodeStream, res);
296
+ }
297
+ }
298
+ } catch (err) {
299
+ console.error('Failed to write response:', err);
300
+ if (!res.headersSent) {
301
+ res.writeHead(502).end('Internal Server Error');
302
+ }
260
303
  }
261
304
  }
262
305
 
@@ -294,16 +337,14 @@ export async function startProxyServer(
294
337
 
295
338
  // 顺序尝试注册表,获取第一个成功响应
296
339
  let successfulResponse: Response | null = null;
340
+ let targetRegistry: RegistryInfo | null = null;
341
+ let targetUrl: string | null = null;
297
342
  for (const registry of registryInfos) {
298
343
  if (req.destroyed) break;
299
-
300
- const response = await fetchFromRegistry(
301
- registry,
302
- path,
303
- fullUrl.search,
304
- limiter
305
- );
306
-
344
+ targetRegistry = registry;
345
+ const search = fullUrl.search || '';
346
+ targetUrl = `${registry.normalizedRegistryUrl}${path}${search}`;
347
+ const response = await fetchFromRegistry(registry, targetUrl, limiter);
307
348
  if (response) {
308
349
  successfulResponse = response;
309
350
  break;
@@ -312,7 +353,7 @@ export async function startProxyServer(
312
353
 
313
354
  // 统一回写响应
314
355
  if (successfulResponse) {
315
- await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
356
+ await writeSuccessfulResponse(targetRegistry!, targetUrl!, res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
316
357
  } else {
317
358
  res.writeHead(404).end('All upstream registries failed');
318
359
  }