com.jimuwd.xian.registry-proxy 1.0.13 → 1.0.15

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