com.jimuwd.xian.registry-proxy 1.0.32 → 1.0.33

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 +40 -13
  2. package/package.json +1 -1
  3. package/src/index.ts +50 -12
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import fetch from 'node-fetch';
7
7
  import { homedir } from 'os';
8
8
  import { join, resolve } from 'path';
9
9
  import { URL } from 'url';
10
+ import { Readable } from "node:stream";
10
11
  const { readFile, writeFile } = fsPromises;
11
12
  class ConcurrencyLimiter {
12
13
  maxConcurrency;
@@ -163,11 +164,12 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
163
164
  // 修改为按顺序尝试注册表,找到第一个成功响应即返回
164
165
  for (const { normalizedRegistryUrl, token } of registryInfos) {
165
166
  await limiter.acquire();
167
+ let response = null;
166
168
  try {
167
169
  const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
168
170
  console.log(`Fetching from: ${targetUrl}`);
169
171
  const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
170
- const response = await fetch(targetUrl, { headers });
172
+ response = await fetch(targetUrl, { headers });
171
173
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
172
174
  if (response.ok) {
173
175
  const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
@@ -202,31 +204,56 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
202
204
  }
203
205
  }
204
206
  else {
205
- // 非application/json则是tarball
206
207
  if (!response.body) {
207
208
  console.error(`Empty response body from ${response.url}, status: ${response.status}`);
208
- // 继续尝试下一个注册表
209
209
  continue;
210
210
  }
211
+ // 设置正确的响应头
211
212
  const contentLength = response.headers.get('Content-Length');
212
- const safeHeaders = {};
213
- safeHeaders["content-type"] = contentType;
214
- if (contentLength && !isNaN(Number(contentLength)))
215
- safeHeaders["content-length"] = contentLength;
216
- res.writeHead(response.status, safeHeaders);
217
- response.body.pipe(res).on('error', (err) => {
218
- console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
219
- res.writeHead(502).end('Stream Error');
213
+ const safeHeaders = {
214
+ 'content-type': contentType,
215
+ 'connection': 'keep-alive'
216
+ };
217
+ if (contentLength && !isNaN(Number(contentLength))) {
218
+ safeHeaders['content-length'] = contentLength;
219
+ }
220
+ // 复制其他可能有用的头信息
221
+ const headersToCopy = ['cache-control', 'etag', 'last-modified'];
222
+ headersToCopy.forEach(header => {
223
+ const value = response?.headers.get(header);
224
+ if (value) {
225
+ safeHeaders[header] = value;
226
+ }
220
227
  });
221
- return;
228
+ res.writeHead(response.status, safeHeaders);
229
+ // 使用 pipeline 处理流
230
+ const { pipeline } = await import('stream');
231
+ const { promisify } = await import('util');
232
+ const pipelineAsync = promisify(pipeline);
233
+ try {
234
+ // 将 ReadableStream 转换为 Node.js Readable
235
+ const nodeStream = Readable.fromWeb(response.body);
236
+ // 双向终止保护
237
+ req.on('close', () => nodeStream.destroy());
238
+ res.on('close', () => nodeStream.destroy());
239
+ await pipelineAsync(nodeStream, res);
240
+ return; // 成功完成管道传输后返回
241
+ }
242
+ catch (pipeError) {
243
+ console.error(`Stream pipeline error for ${relativePathPrefixedWithSlash}:`, pipeError);
244
+ if (!res.headersSent) {
245
+ res.writeHead(502).end('Stream Error');
246
+ }
247
+ continue;
248
+ }
222
249
  }
223
250
  }
224
251
  }
225
252
  catch (e) {
226
253
  console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
227
- // 继续尝试下一个注册表
228
254
  }
229
255
  finally {
256
+ // 不需要手动销毁 ReadableStream
230
257
  limiter.release();
231
258
  }
232
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
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
@@ -8,6 +8,7 @@ import fetch, {Response} from 'node-fetch';
8
8
  import {homedir} from 'os';
9
9
  import {join, resolve} from 'path';
10
10
  import {URL} from 'url';
11
+ import {Readable} from "node:stream";
11
12
 
12
13
  const {readFile, writeFile} = fsPromises;
13
14
 
@@ -222,11 +223,12 @@ export async function startProxyServer(
222
223
  // 修改为按顺序尝试注册表,找到第一个成功响应即返回
223
224
  for (const {normalizedRegistryUrl, token} of registryInfos) {
224
225
  await limiter.acquire();
226
+ let response: Response | null = null;
225
227
  try {
226
228
  const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
227
229
  console.log(`Fetching from: ${targetUrl}`);
228
230
  const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
229
- const response = await fetch(targetUrl, {headers});
231
+ response = await fetch(targetUrl, {headers});
230
232
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
231
233
 
232
234
  if (response.ok) {
@@ -260,28 +262,64 @@ export async function startProxyServer(
260
262
  // 继续尝试下一个注册表
261
263
  }
262
264
  } else {
263
- // 非application/json则是tarball
264
265
  if (!response.body) {
265
266
  console.error(`Empty response body from ${response.url}, status: ${response.status}`);
266
- // 继续尝试下一个注册表
267
267
  continue;
268
268
  }
269
+
270
+ // 设置正确的响应头
269
271
  const contentLength = response.headers.get('Content-Length');
270
- const safeHeaders: OutgoingHttpHeaders = {};
271
- safeHeaders["content-type"] = contentType;
272
- if (contentLength && !isNaN(Number(contentLength))) safeHeaders["content-length"] = contentLength;
273
- res.writeHead(response.status, safeHeaders);
274
- response.body.pipe(res).on('error', (err: any) => {
275
- console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
276
- res.writeHead(502).end('Stream Error');
272
+ const safeHeaders: OutgoingHttpHeaders = {
273
+ 'content-type': contentType,
274
+ 'connection': 'keep-alive'
275
+ };
276
+
277
+ if (contentLength && !isNaN(Number(contentLength))) {
278
+ safeHeaders['content-length'] = contentLength;
279
+ }
280
+
281
+ // 复制其他可能有用的头信息
282
+ const headersToCopy = ['cache-control', 'etag', 'last-modified'];
283
+ headersToCopy.forEach(header => {
284
+ const value = response?.headers.get(header);
285
+ if (value) {
286
+ safeHeaders[header] = value;
287
+ }
277
288
  });
278
- return;
289
+
290
+ res.writeHead(response.status, safeHeaders);
291
+
292
+ // 使用 pipeline 处理流
293
+ const {pipeline} = await import('stream');
294
+ const {promisify} = await import('util');
295
+ const pipelineAsync = promisify(pipeline);
296
+
297
+ try {
298
+ // 将 ReadableStream 转换为 Node.js Readable
299
+ const nodeStream = Readable.fromWeb(response.body as any);
300
+
301
+ // 双向终止保护
302
+ req.on('close', () => nodeStream.destroy());
303
+ res.on('close', () => nodeStream.destroy());
304
+
305
+ await pipelineAsync(
306
+ nodeStream,
307
+ res
308
+ );
309
+ return; // 成功完成管道传输后返回
310
+ } catch (pipeError) {
311
+ console.error(`Stream pipeline error for ${relativePathPrefixedWithSlash}:`, pipeError);
312
+ if (!res.headersSent) {
313
+ res.writeHead(502).end('Stream Error');
314
+ }
315
+ continue;
316
+ }
279
317
  }
280
318
  }
281
319
  } catch (e) {
282
320
  console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
283
- // 继续尝试下一个注册表
284
321
  } finally {
322
+ // 不需要手动销毁 ReadableStream
285
323
  limiter.release();
286
324
  }
287
325
  }