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

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 +71 -98
  2. package/package.json +1 -1
  3. package/src/index.ts +95 -99
package/dist/index.js CHANGED
@@ -139,6 +139,60 @@ 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
+ 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 {
183
+ const safeHeaders = {
184
+ 'content-type': contentType,
185
+ 'connection': 'keep-alive',
186
+ };
187
+ ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
188
+ const value = response.headers.get(header);
189
+ if (value)
190
+ safeHeaders[header] = value;
191
+ });
192
+ res.writeHead(response.status, safeHeaders);
193
+ await pipeline(Readable.fromWeb(response.body), res);
194
+ }
195
+ }
142
196
  export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
143
197
  const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
144
198
  const registryInfos = proxyInfo.registries;
@@ -149,116 +203,35 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
149
203
  let proxyPort;
150
204
  const requestHandler = async (req, res) => {
151
205
  if (!req.url || !req.headers.host) {
152
- console.error('Invalid request: missing URL or host header');
153
206
  res.writeHead(400).end('Invalid Request');
154
207
  return;
155
208
  }
156
209
  const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
157
- console.log(`Proxy server received request on ${fullUrl.toString()}`);
158
210
  if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
159
- console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
160
211
  res.writeHead(404).end('Not Found');
161
212
  return;
162
213
  }
163
- const relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
164
- console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
165
- // 修改为按顺序尝试注册表,找到第一个成功响应即返回
166
- for (const { normalizedRegistryUrl, token } of registryInfos) {
214
+ const path = basePathPrefixedWithSlash === '/'
215
+ ? fullUrl.pathname
216
+ : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
217
+ // 顺序尝试注册表,获取第一个成功响应
218
+ let successfulResponse = null;
219
+ for (const registry of registryInfos) {
167
220
  if (req.destroyed)
168
221
  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();
222
+ const response = await fetchFromRegistry(registry, path, fullUrl.search, limiter);
223
+ if (response) {
224
+ successfulResponse = response;
225
+ break;
258
226
  }
259
227
  }
260
- // 所有注册表尝试失败
261
- res.writeHead(404).end('All upstream registries failed');
228
+ // 统一回写响应
229
+ if (successfulResponse) {
230
+ await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
231
+ }
232
+ else {
233
+ res.writeHead(404).end('All upstream registries failed');
234
+ }
262
235
  };
263
236
  let server;
264
237
  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.35",
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
@@ -187,6 +187,79 @@ 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
+ async function writeResponse(
219
+ res: ServerResponse,
220
+ response: Response,
221
+ req: IncomingMessage,
222
+ proxyInfo: ProxyInfo,
223
+ proxyPort: number,
224
+ registryInfos: RegistryInfo[]
225
+ ): 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 {
245
+ const safeHeaders: OutgoingHttpHeaders = {
246
+ 'content-type': contentType,
247
+ 'connection': 'keep-alive',
248
+ };
249
+
250
+ ['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
251
+ const value = response.headers.get(header);
252
+ if (value) safeHeaders[header] = value;
253
+ });
254
+
255
+ res.writeHead(response.status, safeHeaders);
256
+ await pipeline(
257
+ Readable.fromWeb(response.body as any),
258
+ res
259
+ );
260
+ }
261
+ }
262
+
190
263
  export async function startProxyServer(
191
264
  proxyConfigPath?: string,
192
265
  localYarnConfigPath?: string,
@@ -205,121 +278,44 @@ export async function startProxyServer(
205
278
 
206
279
  const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
207
280
  if (!req.url || !req.headers.host) {
208
- console.error('Invalid request: missing URL or host header');
209
281
  res.writeHead(400).end('Invalid Request');
210
282
  return;
211
283
  }
212
284
 
213
285
  const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
214
- console.log(`Proxy server received request on ${fullUrl.toString()}`)
215
286
  if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
216
- console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
217
287
  res.writeHead(404).end('Not Found');
218
288
  return;
219
289
  }
220
290
 
221
- const relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
222
- console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
291
+ const path = basePathPrefixedWithSlash === '/'
292
+ ? fullUrl.pathname
293
+ : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
223
294
 
224
- // 修改为按顺序尝试注册表,找到第一个成功响应即返回
225
- for (const {normalizedRegistryUrl, token} of registryInfos) {
295
+ // 顺序尝试注册表,获取第一个成功响应
296
+ let successfulResponse: Response | null = null;
297
+ for (const registry of registryInfos) {
226
298
  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
299
 
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();
300
+ const response = await fetchFromRegistry(
301
+ registry,
302
+ path,
303
+ fullUrl.search,
304
+ limiter
305
+ );
306
+
307
+ if (response) {
308
+ successfulResponse = response;
309
+ break;
318
310
  }
319
311
  }
320
312
 
321
- // 所有注册表尝试失败
322
- res.writeHead(404).end('All upstream registries failed');
313
+ // 统一回写响应
314
+ if (successfulResponse) {
315
+ await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
316
+ } else {
317
+ res.writeHead(404).end('All upstream registries failed');
318
+ }
323
319
  };
324
320
 
325
321
  let server: HttpServer | HttpsServer;