com.jimuwd.xian.registry-proxy 1.0.35 → 1.0.37
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.
- package/dist/index.js +73 -26
- package/package.json +1 -1
- package/src/index.ts +82 -41
package/dist/index.js
CHANGED
|
@@ -139,10 +139,9 @@ 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,
|
|
142
|
+
async function fetchFromRegistry(registry, targetUrl, limiter) {
|
|
143
143
|
await limiter.acquire();
|
|
144
144
|
try {
|
|
145
|
-
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
146
145
|
console.log(`Fetching from: ${targetUrl}`);
|
|
147
146
|
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
148
147
|
const response = await fetch(targetUrl, { headers });
|
|
@@ -161,36 +160,79 @@ async function fetchFromRegistry(registry, path, search, limiter) {
|
|
|
161
160
|
limiter.release();
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 {
|
|
163
|
+
// 修改后的 writeSuccessfulResponse 函数
|
|
164
|
+
async function writeSuccessfulResponse(registryInfo, targetUrl, res, response, req, proxyInfo, proxyPort, registryInfos) {
|
|
165
|
+
if (!response.ok)
|
|
166
|
+
throw new Error("Only 2xx response is supported");
|
|
167
|
+
try {
|
|
168
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
169
|
+
// 准备通用头信息
|
|
183
170
|
const safeHeaders = {
|
|
184
171
|
'content-type': contentType,
|
|
185
|
-
'connection': 'keep-alive'
|
|
172
|
+
'connection': 'keep-alive'
|
|
186
173
|
};
|
|
187
|
-
|
|
174
|
+
// 复制所有可能需要的头信息
|
|
175
|
+
const headersToCopy = [
|
|
176
|
+
'cache-control', 'etag', 'last-modified',
|
|
177
|
+
'content-encoding', 'content-length'
|
|
178
|
+
];
|
|
179
|
+
headersToCopy.forEach(header => {
|
|
188
180
|
const value = response.headers.get(header);
|
|
189
181
|
if (value)
|
|
190
182
|
safeHeaders[header] = value;
|
|
191
183
|
});
|
|
192
184
|
res.writeHead(response.status, safeHeaders);
|
|
193
|
-
|
|
185
|
+
if (contentType.includes('application/json')) {
|
|
186
|
+
// JSON 处理逻辑
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
if (data.versions) {
|
|
189
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
190
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
191
|
+
for (const version in data.versions) {
|
|
192
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
193
|
+
if (tarball) {
|
|
194
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
195
|
+
data.versions[version].dist.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
res.end(JSON.stringify(data));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// 二进制流处理
|
|
203
|
+
if (!response.body) {
|
|
204
|
+
console.error(`Empty response body from ${targetUrl}`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
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
|
+
await pipeline(nodeStream, res);
|
|
228
|
+
}
|
|
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
|
+
}
|
|
194
236
|
}
|
|
195
237
|
}
|
|
196
238
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
@@ -216,10 +258,15 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
216
258
|
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
217
259
|
// 顺序尝试注册表,获取第一个成功响应
|
|
218
260
|
let successfulResponse = null;
|
|
261
|
+
let targetRegistry = null;
|
|
262
|
+
let targetUrl = null;
|
|
219
263
|
for (const registry of registryInfos) {
|
|
220
264
|
if (req.destroyed)
|
|
221
265
|
break;
|
|
222
|
-
|
|
266
|
+
targetRegistry = registry;
|
|
267
|
+
const search = fullUrl.search || '';
|
|
268
|
+
targetUrl = `${registry.normalizedRegistryUrl}${path}${search}`;
|
|
269
|
+
const response = await fetchFromRegistry(registry, targetUrl, limiter);
|
|
223
270
|
if (response) {
|
|
224
271
|
successfulResponse = response;
|
|
225
272
|
break;
|
|
@@ -227,7 +274,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
227
274
|
}
|
|
228
275
|
// 统一回写响应
|
|
229
276
|
if (successfulResponse) {
|
|
230
|
-
await
|
|
277
|
+
await writeSuccessfulResponse(targetRegistry, targetUrl, res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
231
278
|
}
|
|
232
279
|
else {
|
|
233
280
|
res.writeHead(404).end('All upstream registries failed');
|
package/package.json
CHANGED
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
|
|
|
@@ -189,16 +189,14 @@ async function loadProxyInfo(
|
|
|
189
189
|
|
|
190
190
|
async function fetchFromRegistry(
|
|
191
191
|
registry: RegistryInfo,
|
|
192
|
-
|
|
193
|
-
search: string,
|
|
192
|
+
targetUrl: string,
|
|
194
193
|
limiter: ConcurrencyLimiter
|
|
195
194
|
): Promise<Response | null> {
|
|
196
195
|
await limiter.acquire();
|
|
197
196
|
try {
|
|
198
|
-
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
199
197
|
console.log(`Fetching from: ${targetUrl}`);
|
|
200
|
-
const headers = registry.token ? {
|
|
201
|
-
const response = await fetch(targetUrl, {
|
|
198
|
+
const headers = registry.token ? {Authorization: `Bearer ${registry.token}`} : undefined;
|
|
199
|
+
const response = await fetch(targetUrl, {headers});
|
|
202
200
|
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
203
201
|
return response.ok ? response : null;
|
|
204
202
|
} catch (e) {
|
|
@@ -215,7 +213,10 @@ async function fetchFromRegistry(
|
|
|
215
213
|
}
|
|
216
214
|
}
|
|
217
215
|
|
|
218
|
-
|
|
216
|
+
// 修改后的 writeSuccessfulResponse 函数
|
|
217
|
+
async function writeSuccessfulResponse(
|
|
218
|
+
registryInfo: RegistryInfo,
|
|
219
|
+
targetUrl: string,
|
|
219
220
|
res: ServerResponse,
|
|
220
221
|
response: Response,
|
|
221
222
|
req: IncomingMessage,
|
|
@@ -223,40 +224,82 @@ async function writeResponse(
|
|
|
223
224
|
proxyPort: number,
|
|
224
225
|
registryInfos: RegistryInfo[]
|
|
225
226
|
): Promise<void> {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 {
|
|
227
|
+
|
|
228
|
+
if (!response.ok) throw new Error("Only 2xx response is supported");
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
232
|
+
|
|
233
|
+
// 准备通用头信息
|
|
245
234
|
const safeHeaders: OutgoingHttpHeaders = {
|
|
246
235
|
'content-type': contentType,
|
|
247
|
-
'connection': 'keep-alive'
|
|
236
|
+
'connection': 'keep-alive'
|
|
248
237
|
};
|
|
249
238
|
|
|
250
|
-
|
|
239
|
+
// 复制所有可能需要的头信息
|
|
240
|
+
const headersToCopy = [
|
|
241
|
+
'cache-control', 'etag', 'last-modified',
|
|
242
|
+
'content-encoding', 'content-length'
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
headersToCopy.forEach(header => {
|
|
251
246
|
const value = response.headers.get(header);
|
|
252
247
|
if (value) safeHeaders[header] = value;
|
|
253
248
|
});
|
|
254
249
|
|
|
250
|
+
|
|
255
251
|
res.writeHead(response.status, safeHeaders);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
|
|
253
|
+
if (contentType.includes('application/json')) {
|
|
254
|
+
// JSON 处理逻辑
|
|
255
|
+
const data = await response.json() as PackageData;
|
|
256
|
+
if (data.versions) {
|
|
257
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
258
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
259
|
+
|
|
260
|
+
for (const version in data.versions) {
|
|
261
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
262
|
+
if (tarball) {
|
|
263
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
264
|
+
data.versions[version]!.dist!.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
res.end(JSON.stringify(data));
|
|
269
|
+
} else {
|
|
270
|
+
// 二进制流处理
|
|
271
|
+
if (!response.body) {
|
|
272
|
+
console.error(`Empty response body from ${targetUrl}`);
|
|
273
|
+
} else {
|
|
274
|
+
let nodeStream: NodeJS.ReadableStream;
|
|
275
|
+
if (typeof Readable.fromWeb === 'function') {
|
|
276
|
+
// Node.js 17+ 标准方式
|
|
277
|
+
nodeStream = Readable.fromWeb(response.body as any);
|
|
278
|
+
} else {
|
|
279
|
+
// Node.js 16 及以下版本的兼容方案
|
|
280
|
+
const {PassThrough} = await import('stream');
|
|
281
|
+
const passThrough = new PassThrough();
|
|
282
|
+
const reader = (response.body as any).getReader();
|
|
283
|
+
|
|
284
|
+
const pump = async () => {
|
|
285
|
+
const {done, value} = await reader.read();
|
|
286
|
+
if (done) return passThrough.end();
|
|
287
|
+
passThrough.write(value);
|
|
288
|
+
await pump();
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
pump().catch(err => passThrough.destroy(err));
|
|
292
|
+
nodeStream = passThrough;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await pipeline(nodeStream, res);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error('Failed to write response:', err);
|
|
300
|
+
if (!res.headersSent) {
|
|
301
|
+
res.writeHead(502).end('Internal Server Error');
|
|
302
|
+
}
|
|
260
303
|
}
|
|
261
304
|
}
|
|
262
305
|
|
|
@@ -294,16 +337,14 @@ export async function startProxyServer(
|
|
|
294
337
|
|
|
295
338
|
// 顺序尝试注册表,获取第一个成功响应
|
|
296
339
|
let successfulResponse: Response | null = null;
|
|
340
|
+
let targetRegistry: RegistryInfo | null = null;
|
|
341
|
+
let targetUrl: string | null = null;
|
|
297
342
|
for (const registry of registryInfos) {
|
|
298
343
|
if (req.destroyed) break;
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
fullUrl.search,
|
|
304
|
-
limiter
|
|
305
|
-
);
|
|
306
|
-
|
|
344
|
+
targetRegistry = registry;
|
|
345
|
+
const search = fullUrl.search || '';
|
|
346
|
+
targetUrl = `${registry.normalizedRegistryUrl}${path}${search}`;
|
|
347
|
+
const response = await fetchFromRegistry(registry, targetUrl, limiter);
|
|
307
348
|
if (response) {
|
|
308
349
|
successfulResponse = response;
|
|
309
350
|
break;
|
|
@@ -312,7 +353,7 @@ export async function startProxyServer(
|
|
|
312
353
|
|
|
313
354
|
// 统一回写响应
|
|
314
355
|
if (successfulResponse) {
|
|
315
|
-
await
|
|
356
|
+
await writeSuccessfulResponse(targetRegistry!, targetUrl!, res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
316
357
|
} else {
|
|
317
358
|
res.writeHead(404).end('All upstream registries failed');
|
|
318
359
|
}
|