com.jimuwd.xian.registry-proxy 1.0.16 → 1.0.18

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 +15 -40
  2. package/package.json +1 -1
  3. package/src/index.ts +15 -40
package/dist/index.js CHANGED
@@ -8,7 +8,6 @@ import { homedir } from 'os';
8
8
  import { join, resolve } from 'path';
9
9
  import { URL } from 'url';
10
10
  const { readFile, writeFile } = fsPromises;
11
- // 并发控制队列
12
11
  class ConcurrencyLimiter {
13
12
  maxConcurrency;
14
13
  current = 0;
@@ -34,8 +33,7 @@ class ConcurrencyLimiter {
34
33
  }
35
34
  }
36
35
  }
37
- // 设置最大并发数(可调整)
38
- const limiter = new ConcurrencyLimiter(5); // 限制为 5 个并发请求
36
+ const limiter = new ConcurrencyLimiter(3);
39
37
  function normalizeUrl(url) {
40
38
  try {
41
39
  const urlObj = new URL(url);
@@ -45,20 +43,16 @@ function normalizeUrl(url) {
45
43
  else if (urlObj.protocol === 'https:' && (urlObj.port === '443' || urlObj.port === '')) {
46
44
  urlObj.port = '';
47
45
  }
48
- if (!urlObj.pathname.endsWith('/')) {
49
- urlObj.pathname += '/';
50
- }
51
- console.debug(`Normalized URL: ${url} -> ${urlObj.toString()}`);
46
+ urlObj.pathname = urlObj.pathname.replace(/\/+$/, '');
52
47
  return urlObj.toString();
53
48
  }
54
49
  catch (e) {
55
50
  console.error(`Invalid URL: ${url}`, e);
56
- return url.endsWith('/') ? url : `${url}/`;
51
+ return url.replace(/\/+$/, '');
57
52
  }
58
53
  }
59
54
  function resolvePath(path) {
60
- const resolved = path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
61
- return resolved;
55
+ return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
62
56
  }
63
57
  function removeRegistryPrefix(tarballUrl, registries) {
64
58
  try {
@@ -66,15 +60,11 @@ function removeRegistryPrefix(tarballUrl, registries) {
66
60
  const normalizedRegistries = registries
67
61
  .map(r => normalizeUrl(r.url))
68
62
  .sort((a, b) => b.length - a.length);
69
- console.debug(`Removing registry prefix from tarball: ${normalizedTarball}`);
70
63
  for (const registry of normalizedRegistries) {
71
64
  if (normalizedTarball.startsWith(registry)) {
72
- const result = normalizedTarball.slice(registry.length - 1) || '/';
73
- console.debug(`Matched registry ${registry}, result: ${result}`);
74
- return result;
65
+ return normalizedTarball.slice(registry.length) || '/';
75
66
  }
76
67
  }
77
- console.debug(`No registry prefix matched for ${normalizedTarball}`);
78
68
  }
79
69
  catch (e) {
80
70
  console.error(`Invalid URL in removeRegistryPrefix: ${tarballUrl}`, e);
@@ -83,14 +73,12 @@ function removeRegistryPrefix(tarballUrl, registries) {
83
73
  }
84
74
  async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
85
75
  const resolvedPath = resolvePath(proxyConfigPath);
86
- console.debug(`Loading proxy config from: ${resolvedPath}`);
87
76
  try {
88
77
  const content = await readFile(resolvedPath, 'utf8');
89
78
  const config = load(content);
90
79
  if (!config.registries) {
91
80
  throw new Error('Missing required "registries" field in config');
92
81
  }
93
- console.debug('Loaded proxy config:', JSON.stringify(config, null, 2));
94
82
  return config;
95
83
  }
96
84
  catch (e) {
@@ -99,12 +87,9 @@ async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
99
87
  }
100
88
  }
101
89
  async function loadYarnConfig(path) {
102
- console.debug(`Loading Yarn config from: ${path}`);
103
90
  try {
104
91
  const content = await readFile(resolvePath(path), 'utf8');
105
- const config = load(content);
106
- console.debug(`Loaded Yarn config from ${path}:`, JSON.stringify(config, null, 2));
107
- return config;
92
+ return load(content);
108
93
  }
109
94
  catch (e) {
110
95
  console.warn(`Failed to load Yarn config from ${path}:`, e);
@@ -112,7 +97,6 @@ async function loadYarnConfig(path) {
112
97
  }
113
98
  }
114
99
  async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYarnConfigPath = './.yarnrc.yml', globalYarnConfigPath = join(homedir(), '.yarnrc.yml')) {
115
- console.debug('Loading registries...');
116
100
  const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
117
101
  loadProxyConfig(proxyConfigPath),
118
102
  loadYarnConfig(localYarnConfigPath),
@@ -129,16 +113,13 @@ async function loadRegistries(proxyConfigPath = './.registry-proxy.yml', localYa
129
113
  config.npmRegistries?.[url];
130
114
  if (registryConfig?.npmAuthToken) {
131
115
  token = registryConfig.npmAuthToken;
132
- console.debug(`Found token for ${normalizedUrl} in Yarn config`);
133
116
  break;
134
117
  }
135
118
  }
136
119
  }
137
120
  registryMap.set(normalizedUrl, { url: normalizedUrl, token });
138
121
  }
139
- const registries = Array.from(registryMap.values());
140
- console.log('Loaded registries:', registries);
141
- return registries;
122
+ return Array.from(registryMap.values());
142
123
  }
143
124
  export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
144
125
  const proxyConfig = await loadProxyConfig(proxyConfigPath);
@@ -149,14 +130,12 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
149
130
  console.log('HTTPS:', !!proxyConfig.https);
150
131
  let proxyPort;
151
132
  const requestHandler = async (req, res) => {
152
- console.debug(`Received request: ${req.method} ${req.url}`);
153
133
  if (!req.url || !req.headers.host) {
154
134
  console.error('Invalid request: missing URL or host header');
155
135
  res.writeHead(400).end('Invalid Request');
156
136
  return;
157
137
  }
158
138
  const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
159
- console.debug(`Full URL: ${fullUrl.toString()}`);
160
139
  if (basePath && !fullUrl.pathname.startsWith(basePath)) {
161
140
  console.error(`Path ${fullUrl.pathname} does not match basePath ${basePath}`);
162
141
  res.writeHead(404).end('Not Found');
@@ -167,13 +146,13 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
167
146
  : fullUrl.pathname;
168
147
  console.log(`Proxying: ${relativePath}`);
169
148
  const fetchPromises = registries.map(async ({ url, token }) => {
170
- await limiter.acquire(); // 获取并发许可
149
+ await limiter.acquire();
171
150
  try {
172
- const cleanRelativePath = relativePath.replace(/\/+$/, '');
173
- const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
151
+ const cleanRelativePath = relativePath.replace(/^\/+|\/+$/g, '');
152
+ const targetUrl = `${url}/${cleanRelativePath}${fullUrl.search || ''}`.replace(/\/+/g, '/');
174
153
  console.log(`Fetching from: ${targetUrl}`);
175
154
  const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
176
- const response = await fetch(targetUrl, { headers });
155
+ const response = await fetch(targetUrl, { headers, });
177
156
  console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
178
157
  return response.ok ? response : null;
179
158
  }
@@ -182,7 +161,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
182
161
  return null;
183
162
  }
184
163
  finally {
185
- limiter.release(); // 释放并发许可
164
+ limiter.release();
186
165
  }
187
166
  });
188
167
  const responses = await Promise.all(fetchPromises);
@@ -198,20 +177,17 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
198
177
  const data = await successResponse.json();
199
178
  if (data.versions) {
200
179
  const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
201
- console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
202
180
  for (const version in data.versions) {
203
181
  const dist = data.versions[version]?.dist;
204
182
  if (dist?.tarball) {
205
183
  const originalUrl = new URL(dist.tarball);
206
184
  const tarballPath = removeRegistryPrefix(dist.tarball, registries);
207
185
  dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
208
- console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
209
186
  }
210
187
  }
211
188
  }
212
189
  res.writeHead(200, { 'Content-Type': 'application/json' });
213
- const jsonResponse = JSON.stringify(data);
214
- res.end(jsonResponse);
190
+ res.end(JSON.stringify(data));
215
191
  }
216
192
  catch (e) {
217
193
  console.error('Failed to parse JSON response:', e);
@@ -224,9 +200,10 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
224
200
  res.writeHead(502).end('Empty Response Body');
225
201
  return;
226
202
  }
203
+ const contentLength = successResponse.headers.get('Content-Length');
227
204
  const safeHeaders = {
228
205
  'Content-Type': successResponse.headers.get('Content-Type'),
229
- 'Content-Length': successResponse.headers.get('Content-Length'),
206
+ 'Content-Length': contentLength && !isNaN(Number(contentLength)) ? contentLength : undefined,
230
207
  };
231
208
  res.writeHead(successResponse.status, safeHeaders);
232
209
  successResponse.body.pipe(res).on('error', (err) => {
@@ -270,7 +247,6 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
270
247
  const address = server.address();
271
248
  proxyPort = address.port;
272
249
  const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
273
- console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
274
250
  writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
275
251
  console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
276
252
  resolve(server);
@@ -279,7 +255,6 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
279
255
  }
280
256
  if (import.meta.url === `file://${process.argv[1]}`) {
281
257
  const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
282
- console.log(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
283
258
  startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
284
259
  console.error('Failed to start server:', err);
285
260
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
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
@@ -23,7 +23,6 @@ interface RegistryInfo { url: string; token?: string; }
23
23
  interface PackageVersion { dist?: { tarball?: string }; }
24
24
  interface PackageData { versions?: Record<string, PackageVersion>; }
25
25
 
26
- // 并发控制队列
27
26
  class ConcurrencyLimiter {
28
27
  private maxConcurrency: number;
29
28
  private current: number = 0;
@@ -53,8 +52,7 @@ class ConcurrencyLimiter {
53
52
  }
54
53
  }
55
54
 
56
- // 设置最大并发数(可调整)
57
- const limiter = new ConcurrencyLimiter(5); // 限制为 5 个并发请求
55
+ const limiter = new ConcurrencyLimiter(3);
58
56
 
59
57
  function normalizeUrl(url: string): string {
60
58
  try {
@@ -64,20 +62,16 @@ function normalizeUrl(url: string): string {
64
62
  } else if (urlObj.protocol === 'https:' && (urlObj.port === '443' || urlObj.port === '')) {
65
63
  urlObj.port = '';
66
64
  }
67
- if (!urlObj.pathname.endsWith('/')) {
68
- urlObj.pathname += '/';
69
- }
70
- console.debug(`Normalized URL: ${url} -> ${urlObj.toString()}`);
65
+ urlObj.pathname = urlObj.pathname.replace(/\/+$/, '');
71
66
  return urlObj.toString();
72
67
  } catch (e) {
73
68
  console.error(`Invalid URL: ${url}`, e);
74
- return url.endsWith('/') ? url : `${url}/`;
69
+ return url.replace(/\/+$/, '');
75
70
  }
76
71
  }
77
72
 
78
73
  function resolvePath(path: string): string {
79
- const resolved = path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
80
- return resolved;
74
+ return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
81
75
  }
82
76
 
83
77
  function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
@@ -87,15 +81,11 @@ function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): s
87
81
  .map(r => normalizeUrl(r.url))
88
82
  .sort((a, b) => b.length - a.length);
89
83
 
90
- console.debug(`Removing registry prefix from tarball: ${normalizedTarball}`);
91
84
  for (const registry of normalizedRegistries) {
92
85
  if (normalizedTarball.startsWith(registry)) {
93
- const result = normalizedTarball.slice(registry.length - 1) || '/';
94
- console.debug(`Matched registry ${registry}, result: ${result}`);
95
- return result;
86
+ return normalizedTarball.slice(registry.length) || '/';
96
87
  }
97
88
  }
98
- console.debug(`No registry prefix matched for ${normalizedTarball}`);
99
89
  } catch (e) {
100
90
  console.error(`Invalid URL in removeRegistryPrefix: ${tarballUrl}`, e);
101
91
  }
@@ -104,14 +94,12 @@ function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): s
104
94
 
105
95
  async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml'): Promise<ProxyConfig> {
106
96
  const resolvedPath = resolvePath(proxyConfigPath);
107
- console.debug(`Loading proxy config from: ${resolvedPath}`);
108
97
  try {
109
98
  const content = await readFile(resolvedPath, 'utf8');
110
99
  const config = load(content) as ProxyConfig;
111
100
  if (!config.registries) {
112
101
  throw new Error('Missing required "registries" field in config');
113
102
  }
114
- console.debug('Loaded proxy config:', JSON.stringify(config, null, 2));
115
103
  return config;
116
104
  } catch (e) {
117
105
  console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
@@ -120,12 +108,9 @@ async function loadProxyConfig(proxyConfigPath = './.registry-proxy.yml'): Promi
120
108
  }
121
109
 
122
110
  async function loadYarnConfig(path: string): Promise<YarnConfig> {
123
- console.debug(`Loading Yarn config from: ${path}`);
124
111
  try {
125
112
  const content = await readFile(resolvePath(path), 'utf8');
126
- const config = load(content) as YarnConfig;
127
- console.debug(`Loaded Yarn config from ${path}:`, JSON.stringify(config, null, 2));
128
- return config;
113
+ return load(content) as YarnConfig;
129
114
  } catch (e) {
130
115
  console.warn(`Failed to load Yarn config from ${path}:`, e);
131
116
  return {};
@@ -137,7 +122,6 @@ async function loadRegistries(
137
122
  localYarnConfigPath = './.yarnrc.yml',
138
123
  globalYarnConfigPath = join(homedir(), '.yarnrc.yml')
139
124
  ): Promise<RegistryInfo[]> {
140
- console.debug('Loading registries...');
141
125
  const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
142
126
  loadProxyConfig(proxyConfigPath),
143
127
  loadYarnConfig(localYarnConfigPath),
@@ -156,16 +140,13 @@ async function loadRegistries(
156
140
  config.npmRegistries?.[url];
157
141
  if (registryConfig?.npmAuthToken) {
158
142
  token = registryConfig.npmAuthToken;
159
- console.debug(`Found token for ${normalizedUrl} in Yarn config`);
160
143
  break;
161
144
  }
162
145
  }
163
146
  }
164
147
  registryMap.set(normalizedUrl, { url: normalizedUrl, token });
165
148
  }
166
- const registries = Array.from(registryMap.values());
167
- console.log('Loaded registries:', registries);
168
- return registries;
149
+ return Array.from(registryMap.values());
169
150
  }
170
151
 
171
152
  export async function startProxyServer(
@@ -185,7 +166,6 @@ export async function startProxyServer(
185
166
  let proxyPort: number;
186
167
 
187
168
  const requestHandler = async (req: any, res: any) => {
188
- console.debug(`Received request: ${req.method} ${req.url}`);
189
169
  if (!req.url || !req.headers.host) {
190
170
  console.error('Invalid request: missing URL or host header');
191
171
  res.writeHead(400).end('Invalid Request');
@@ -193,7 +173,6 @@ export async function startProxyServer(
193
173
  }
194
174
 
195
175
  const fullUrl = new URL(req.url, `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host}`);
196
- console.debug(`Full URL: ${fullUrl.toString()}`);
197
176
  if (basePath && !fullUrl.pathname.startsWith(basePath)) {
198
177
  console.error(`Path ${fullUrl.pathname} does not match basePath ${basePath}`);
199
178
  res.writeHead(404).end('Not Found');
@@ -206,20 +185,20 @@ export async function startProxyServer(
206
185
  console.log(`Proxying: ${relativePath}`);
207
186
 
208
187
  const fetchPromises = registries.map(async ({ url, token }) => {
209
- await limiter.acquire(); // 获取并发许可
188
+ await limiter.acquire();
210
189
  try {
211
- const cleanRelativePath = relativePath.replace(/\/+$/, '');
212
- const targetUrl = `${url}${cleanRelativePath}${fullUrl.search || ''}`;
190
+ const cleanRelativePath = relativePath.replace(/^\/+|\/+$/g, '');
191
+ const targetUrl = `${url}/${cleanRelativePath}${fullUrl.search || ''}`.replace(/\/+/g, '/');
213
192
  console.log(`Fetching from: ${targetUrl}`);
214
193
  const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
215
- const response = await fetch(targetUrl, { headers });
194
+ const response = await fetch(targetUrl, { headers, });
216
195
  console.log(`Response from ${url}: ${response.status} ${response.statusText}`);
217
196
  return response.ok ? response : null;
218
197
  } catch (e) {
219
198
  console.error(`Failed to fetch from ${url}:`, e);
220
199
  return null;
221
200
  } finally {
222
- limiter.release(); // 释放并发许可
201
+ limiter.release();
223
202
  }
224
203
  });
225
204
 
@@ -237,20 +216,17 @@ export async function startProxyServer(
237
216
  const data = await successResponse.json() as PackageData;
238
217
  if (data.versions) {
239
218
  const proxyBase = `${proxyConfig.https ? 'https' : 'http'}://${req.headers.host || 'localhost:' + proxyPort}${basePath}`;
240
- console.debug(`Rewriting tarball URLs with proxy base: ${proxyBase}`);
241
219
  for (const version in data.versions) {
242
220
  const dist = data.versions[version]?.dist;
243
221
  if (dist?.tarball) {
244
222
  const originalUrl = new URL(dist.tarball);
245
223
  const tarballPath = removeRegistryPrefix(dist.tarball, registries);
246
224
  dist.tarball = `${proxyBase}${tarballPath}${originalUrl.search || ''}`;
247
- console.debug(`Rewrote tarball: ${originalUrl} -> ${dist.tarball}`);
248
225
  }
249
226
  }
250
227
  }
251
228
  res.writeHead(200, { 'Content-Type': 'application/json' });
252
- const jsonResponse = JSON.stringify(data);
253
- res.end(jsonResponse);
229
+ res.end(JSON.stringify(data));
254
230
  } catch (e) {
255
231
  console.error('Failed to parse JSON response:', e);
256
232
  res.writeHead(502).end('Invalid Upstream Response');
@@ -261,9 +237,10 @@ export async function startProxyServer(
261
237
  res.writeHead(502).end('Empty Response Body');
262
238
  return;
263
239
  }
240
+ const contentLength = successResponse.headers.get('Content-Length');
264
241
  const safeHeaders = {
265
242
  'Content-Type': successResponse.headers.get('Content-Type'),
266
- 'Content-Length': successResponse.headers.get('Content-Length'),
243
+ 'Content-Length': contentLength && !isNaN(Number(contentLength)) ? contentLength : undefined,
267
244
  };
268
245
  res.writeHead(successResponse.status, safeHeaders);
269
246
  successResponse.body.pipe(res).on('error', (err:any) => {
@@ -307,7 +284,6 @@ export async function startProxyServer(
307
284
  const address = server.address() as AddressInfo;
308
285
  proxyPort = address.port;
309
286
  const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
310
- console.debug(`Writing port ${proxyPort} to file: ${portFile}`);
311
287
  writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
312
288
  console.log(`Proxy server running on ${proxyConfig.https ? 'https' : 'http'}://localhost:${proxyPort}${basePath}`);
313
289
  resolve(server);
@@ -317,7 +293,6 @@ export async function startProxyServer(
317
293
 
318
294
  if (import.meta.url === `file://${process.argv[1]}`) {
319
295
  const [,, configPath, localYarnPath, globalYarnPath, port] = process.argv;
320
- console.log(`Starting server with args: configPath=${configPath}, localYarnPath=${localYarnPath}, globalYarnPath=${globalYarnPath}, port=${port}`);
321
296
  startProxyServer(
322
297
  configPath,
323
298
  localYarnPath,