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

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 +66 -64
  2. package/package.json +1 -1
  3. package/src/index.ts +66 -62
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ function resolvePath(path) {
69
69
  function removeRegistryPrefix(tarballUrl, registries) {
70
70
  const normalizedTarball = normalizeUrl(tarballUrl);
71
71
  const normalizedRegistries = registries
72
- .map(r => normalizeUrl(r.url))
72
+ .map(r => normalizeUrl(r.normalizedRegistryUrl))
73
73
  .sort((a, b) => b.length - a.length);
74
74
  for (const normalizedRegistry of normalizedRegistries) {
75
75
  if (normalizedTarball.startsWith(normalizedRegistry)) {
@@ -130,7 +130,7 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
130
130
  }
131
131
  }
132
132
  }
133
- registryMap.set(normalizedProxiedRegUrl, { url: normalizedProxiedRegUrl, token });
133
+ registryMap.set(normalizedProxiedRegUrl, { normalizedRegistryUrl: normalizedProxiedRegUrl, token });
134
134
  }
135
135
  const registries = Array.from(registryMap.values());
136
136
  const https = proxyConfig.https;
@@ -139,9 +139,9 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
139
139
  }
140
140
  export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
141
141
  const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
142
- const registries = proxyInfo.registries;
142
+ const registryInfos = proxyInfo.registries;
143
143
  const basePathPrefixedWithSlash = removeEndingSlashAndForceStartingSlash(proxyInfo.basePath);
144
- console.log('Active registries:', registries.map(r => r.url));
144
+ console.log('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
145
145
  console.log('Proxy base path:', basePathPrefixedWithSlash);
146
146
  console.log('HTTPS:', !!proxyInfo.https);
147
147
  let proxyPort;
@@ -160,78 +160,79 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
160
160
  }
161
161
  const relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
162
162
  console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
163
- const fetchPromises = registries.map(async ({ url, token }) => {
163
+ // 修改为按顺序尝试注册表,找到第一个成功响应即返回
164
+ for (const { normalizedRegistryUrl, token } of registryInfos) {
164
165
  await limiter.acquire();
165
166
  try {
166
- const cleanRelativePath = relativePathPrefixedWithSlash.replace(/^\/+|\/+$/g, '');
167
- const targetUrl = `${url}/${cleanRelativePath}${fullUrl.search || ''}`;
167
+ const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
168
168
  console.log(`Fetching from: ${targetUrl}`);
169
169
  const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
170
- const response = await fetch(targetUrl, { headers, });
170
+ const response = await fetch(targetUrl, { headers });
171
171
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
172
- return response.ok ? response : null;
173
- }
174
- catch (e) {
175
- console.error(`Failed to fetch from ${url}:`, e);
176
- return null;
177
- }
178
- finally {
179
- limiter.release();
180
- }
181
- });
182
- const responses = await Promise.all(fetchPromises);
183
- const successResponse = responses.find((r) => r !== null);
184
- if (!successResponse) {
185
- console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
186
- res.writeHead(404).end('Not Found - All upstream registries failed');
187
- return;
188
- }
189
- const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
190
- if (contentType.includes('application/json')) {
191
- try {
192
- const data = await successResponse.json();
193
- if (data.versions) {
194
- const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
195
- console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
196
- const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
197
- console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
198
- for (const version in data.versions) {
199
- const dist = data.versions[version]?.dist;
200
- if (dist?.tarball) {
201
- const originalUrl = new URL(dist.tarball);
202
- const originalSearchParamsStr = originalUrl.search || '';
203
- const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registries);
204
- dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
205
- if (!tarballPathPrefixedWithSlash.startsWith("/"))
206
- console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
172
+ if (response.ok) {
173
+ const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
174
+ if (contentType.includes('application/json')) {
175
+ // application/json 元数据
176
+ try {
177
+ const data = await response.json();
178
+ if (data.versions) {
179
+ const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
180
+ console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
181
+ const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
182
+ console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
183
+ for (const version in data.versions) {
184
+ const dist = data.versions[version]?.dist;
185
+ if (dist?.tarball) {
186
+ const originalUrl = new URL(dist.tarball);
187
+ const originalSearchParamsStr = originalUrl.search || '';
188
+ const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
189
+ dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
190
+ if (!tarballPathPrefixedWithSlash.startsWith("/"))
191
+ console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
192
+ }
193
+ }
194
+ }
195
+ res.writeHead(200, { 'Content-Type': 'application/json' });
196
+ res.end(JSON.stringify(data));
197
+ return;
198
+ }
199
+ catch (e) {
200
+ console.error('Failed to parse JSON response:', e);
201
+ // 继续尝试下一个注册表
202
+ }
203
+ }
204
+ else {
205
+ // 非application/json则是tarball
206
+ if (!response.body) {
207
+ console.error(`Empty response body from ${response.url}, status: ${response.status}`);
208
+ // 继续尝试下一个注册表
209
+ continue;
207
210
  }
211
+ 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');
220
+ });
221
+ return;
208
222
  }
209
223
  }
210
- res.writeHead(200, { 'Content-Type': 'application/json' });
211
- res.end(JSON.stringify(data));
212
224
  }
213
225
  catch (e) {
214
- console.error('Failed to parse JSON response:', e);
215
- res.writeHead(502).end('Invalid Upstream Response');
226
+ console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
227
+ // 继续尝试下一个注册表
216
228
  }
217
- }
218
- else {
219
- if (!successResponse.body) {
220
- console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
221
- res.writeHead(502).end('Empty Response Body');
222
- return;
229
+ finally {
230
+ limiter.release();
223
231
  }
224
- const contentLength = successResponse.headers.get('Content-Length');
225
- const safeHeaders = {};
226
- safeHeaders["content-type"] = contentType;
227
- if (contentLength && !isNaN(Number(contentLength)))
228
- safeHeaders["content-length"] = contentLength;
229
- res.writeHead(successResponse.status, safeHeaders);
230
- successResponse.body.pipe(res).on('error', (err) => {
231
- console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
232
- res.writeHead(502).end('Stream Error');
233
- });
234
232
  }
233
+ // 所有注册表都尝试失败
234
+ console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
235
+ res.writeHead(404).end('Not Found - All upstream registries failed');
235
236
  };
236
237
  let server;
237
238
  if (proxyInfo.https) {
@@ -255,7 +256,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
255
256
  else {
256
257
  server = createServer(requestHandler);
257
258
  }
258
- return new Promise((resolve, reject) => {
259
+ const promisedServer = new Promise((resolve, reject) => {
259
260
  server.on('error', (err) => {
260
261
  if (err.code === 'EADDRINUSE') {
261
262
  console.error(`Port ${port} is in use, please specify a different port or free it.`);
@@ -273,6 +274,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
273
274
  resolve(server);
274
275
  });
275
276
  });
277
+ return promisedServer;
276
278
  }
277
279
  if (import.meta.url === `file://${process.argv[1]}`) {
278
280
  const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
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
@@ -31,7 +31,7 @@ interface YarnConfig {
31
31
  }
32
32
 
33
33
  interface RegistryInfo {
34
- url: string;
34
+ normalizedRegistryUrl: string;
35
35
  token?: string;
36
36
  }
37
37
 
@@ -111,7 +111,7 @@ function resolvePath(path: string): string {
111
111
  function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
112
112
  const normalizedTarball = normalizeUrl(tarballUrl);
113
113
  const normalizedRegistries = registries
114
- .map(r => normalizeUrl(r.url))
114
+ .map(r => normalizeUrl(r.normalizedRegistryUrl))
115
115
  .sort((a, b) => b.length - a.length);
116
116
  for (const normalizedRegistry of normalizedRegistries) {
117
117
  if (normalizedTarball.startsWith(normalizedRegistry)) {
@@ -177,7 +177,7 @@ async function loadProxyInfo(
177
177
  }
178
178
  }
179
179
  }
180
- registryMap.set(normalizedProxiedRegUrl, {url: normalizedProxiedRegUrl, token});
180
+ registryMap.set(normalizedProxiedRegUrl, {normalizedRegistryUrl: normalizedProxiedRegUrl, token});
181
181
  }
182
182
  const registries = Array.from(registryMap.values());
183
183
  const https = proxyConfig.https;
@@ -192,10 +192,10 @@ export async function startProxyServer(
192
192
  port: number = 0
193
193
  ): Promise<HttpServer | HttpsServer> {
194
194
  const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
195
- const registries = proxyInfo.registries;
195
+ const registryInfos = proxyInfo.registries;
196
196
  const basePathPrefixedWithSlash: string = removeEndingSlashAndForceStartingSlash(proxyInfo.basePath);
197
197
 
198
- console.log('Active registries:', registries.map(r => r.url));
198
+ console.log('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
199
199
  console.log('Proxy base path:', basePathPrefixedWithSlash);
200
200
  console.log('HTTPS:', !!proxyInfo.https);
201
201
 
@@ -219,74 +219,76 @@ export async function startProxyServer(
219
219
  const relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
220
220
  console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
221
221
 
222
- const fetchPromises = registries.map(async ({url, token}) => {
222
+ // 修改为按顺序尝试注册表,找到第一个成功响应即返回
223
+ for (const {normalizedRegistryUrl, token} of registryInfos) {
223
224
  await limiter.acquire();
224
225
  try {
225
- const cleanRelativePath = relativePathPrefixedWithSlash.replace(/^\/+|\/+$/g, '');
226
- const targetUrl = `${url}/${cleanRelativePath}${fullUrl.search || ''}`;
226
+ const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
227
227
  console.log(`Fetching from: ${targetUrl}`);
228
228
  const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
229
- const response = await fetch(targetUrl, {headers,});
229
+ const response = await fetch(targetUrl, {headers});
230
230
  console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
231
- return response.ok ? response : null;
232
- } catch (e) {
233
- console.error(`Failed to fetch from ${url}:`, e);
234
- return null;
235
- } finally {
236
- limiter.release();
237
- }
238
- });
239
-
240
- const responses = await Promise.all(fetchPromises);
241
- const successResponse = responses.find((r): r is Response => r !== null);
242
- if (!successResponse) {
243
- console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
244
- res.writeHead(404).end('Not Found - All upstream registries failed');
245
- return;
246
- }
247
231
 
248
- const contentType = successResponse.headers.get('Content-Type') || 'application/octet-stream';
249
- if (contentType.includes('application/json')) {
250
- try {
251
- const data = await successResponse.json() as PackageData;
252
- if (data.versions) {
253
- const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
254
- console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
255
- const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
256
- console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
257
- for (const version in data.versions) {
258
- const dist = data.versions[version]?.dist;
259
- if (dist?.tarball) {
260
- const originalUrl = new URL(dist.tarball);
261
- const originalSearchParamsStr = originalUrl.search || '';
262
- const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registries);
263
- dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
264
- if (!tarballPathPrefixedWithSlash.startsWith("/")) console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
232
+ if (response.ok) {
233
+ const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
234
+
235
+ if (contentType.includes('application/json')) {
236
+ // application/json 元数据
237
+ try {
238
+ const data = await response.json() as PackageData;
239
+ if (data.versions) {
240
+ const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
241
+ console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
242
+ const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
243
+ console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
244
+ for (const version in data.versions) {
245
+ const dist = data.versions[version]?.dist;
246
+ if (dist?.tarball) {
247
+ const originalUrl = new URL(dist.tarball);
248
+ const originalSearchParamsStr = originalUrl.search || '';
249
+ const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
250
+ dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
251
+ if (!tarballPathPrefixedWithSlash.startsWith("/")) console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
252
+ }
253
+ }
254
+ }
255
+ res.writeHead(200, {'Content-Type': 'application/json'});
256
+ res.end(JSON.stringify(data));
257
+ return;
258
+ } catch (e) {
259
+ console.error('Failed to parse JSON response:', e);
260
+ // 继续尝试下一个注册表
261
+ }
262
+ } else {
263
+ // 非application/json则是tarball
264
+ if (!response.body) {
265
+ console.error(`Empty response body from ${response.url}, status: ${response.status}`);
266
+ // 继续尝试下一个注册表
267
+ continue;
265
268
  }
269
+ 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');
277
+ });
278
+ return;
266
279
  }
267
280
  }
268
- res.writeHead(200, {'Content-Type': 'application/json'});
269
- res.end(JSON.stringify(data));
270
281
  } catch (e) {
271
- console.error('Failed to parse JSON response:', e);
272
- res.writeHead(502).end('Invalid Upstream Response');
273
- }
274
- } else {
275
- if (!successResponse.body) {
276
- console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
277
- res.writeHead(502).end('Empty Response Body');
278
- return;
282
+ console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
283
+ // 继续尝试下一个注册表
284
+ } finally {
285
+ limiter.release();
279
286
  }
280
- const contentLength = successResponse.headers.get('Content-Length');
281
- const safeHeaders: OutgoingHttpHeaders = {};
282
- safeHeaders["content-type"] = contentType;
283
- if (contentLength && !isNaN(Number(contentLength))) safeHeaders["content-length"] = contentLength;
284
- res.writeHead(successResponse.status, safeHeaders);
285
- successResponse.body.pipe(res).on('error', (err: any) => {
286
- console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
287
- res.writeHead(502).end('Stream Error');
288
- });
289
287
  }
288
+
289
+ // 所有注册表都尝试失败
290
+ console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
291
+ res.writeHead(404).end('Not Found - All upstream registries failed');
290
292
  };
291
293
 
292
294
  let server: HttpServer | HttpsServer;
@@ -310,7 +312,7 @@ export async function startProxyServer(
310
312
  server = createServer(requestHandler);
311
313
  }
312
314
 
313
- return new Promise((resolve, reject) => {
315
+ const promisedServer: Promise<HttpServer | HttpsServer> = new Promise((resolve, reject) => {
314
316
  server.on('error', (err: NodeJS.ErrnoException) => {
315
317
  if (err.code === 'EADDRINUSE') {
316
318
  console.error(`Port ${port} is in use, please specify a different port or free it.`);
@@ -328,6 +330,8 @@ export async function startProxyServer(
328
330
  resolve(server);
329
331
  });
330
332
  });
333
+
334
+ return promisedServer as Promise<HttpServer | HttpsServer>;
331
335
  }
332
336
 
333
337
  if (import.meta.url === `file://${process.argv[1]}`) {