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.
Files changed (3) hide show
  1. package/dist/index.js +113 -98
  2. package/package.json +1 -1
  3. 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 relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
164
- console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
165
- // 修改为按顺序尝试注册表,找到第一个成功响应即返回
166
- for (const { normalizedRegistryUrl, token } of registryInfos) {
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.acquire();
170
- let response = null;
171
- try {
172
- const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
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
- res.writeHead(404).end('All upstream registries failed');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
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
 
@@ -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 relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
222
- console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
332
+ const path = basePathPrefixedWithSlash === '/'
333
+ ? fullUrl.pathname
334
+ : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
223
335
 
224
- // 修改为按顺序尝试注册表,找到第一个成功响应即返回
225
- for (const {normalizedRegistryUrl, token} of registryInfos) {
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
- const safeHeaders: OutgoingHttpHeaders = {
275
- 'content-type': contentType,
276
- 'connection': 'keep-alive',
277
- };
278
-
279
- // 复制关键头信息
280
- ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
281
- const value = response?.headers.get(header);
282
- if (value) safeHeaders[header] = value;
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
- res.writeHead(404).end('All upstream registries failed');
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;