com.jimuwd.xian.registry-proxy 1.0.32 → 1.0.33
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 +40 -13
- package/package.json +1 -1
- package/src/index.ts +50 -12
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { join, resolve } from 'path';
|
|
9
9
|
import { URL } from 'url';
|
|
10
|
+
import { Readable } from "node:stream";
|
|
10
11
|
const { readFile, writeFile } = fsPromises;
|
|
11
12
|
class ConcurrencyLimiter {
|
|
12
13
|
maxConcurrency;
|
|
@@ -163,11 +164,12 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
163
164
|
// 修改为按顺序尝试注册表,找到第一个成功响应即返回
|
|
164
165
|
for (const { normalizedRegistryUrl, token } of registryInfos) {
|
|
165
166
|
await limiter.acquire();
|
|
167
|
+
let response = null;
|
|
166
168
|
try {
|
|
167
169
|
const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
|
|
168
170
|
console.log(`Fetching from: ${targetUrl}`);
|
|
169
171
|
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
170
|
-
|
|
172
|
+
response = await fetch(targetUrl, { headers });
|
|
171
173
|
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
172
174
|
if (response.ok) {
|
|
173
175
|
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
@@ -202,31 +204,56 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
else {
|
|
205
|
-
// 非application/json则是tarball
|
|
206
207
|
if (!response.body) {
|
|
207
208
|
console.error(`Empty response body from ${response.url}, status: ${response.status}`);
|
|
208
|
-
// 继续尝试下一个注册表
|
|
209
209
|
continue;
|
|
210
210
|
}
|
|
211
|
+
// 设置正确的响应头
|
|
211
212
|
const contentLength = response.headers.get('Content-Length');
|
|
212
|
-
const safeHeaders = {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
const safeHeaders = {
|
|
214
|
+
'content-type': contentType,
|
|
215
|
+
'connection': 'keep-alive'
|
|
216
|
+
};
|
|
217
|
+
if (contentLength && !isNaN(Number(contentLength))) {
|
|
218
|
+
safeHeaders['content-length'] = contentLength;
|
|
219
|
+
}
|
|
220
|
+
// 复制其他可能有用的头信息
|
|
221
|
+
const headersToCopy = ['cache-control', 'etag', 'last-modified'];
|
|
222
|
+
headersToCopy.forEach(header => {
|
|
223
|
+
const value = response?.headers.get(header);
|
|
224
|
+
if (value) {
|
|
225
|
+
safeHeaders[header] = value;
|
|
226
|
+
}
|
|
220
227
|
});
|
|
221
|
-
|
|
228
|
+
res.writeHead(response.status, safeHeaders);
|
|
229
|
+
// 使用 pipeline 处理流
|
|
230
|
+
const { pipeline } = await import('stream');
|
|
231
|
+
const { promisify } = await import('util');
|
|
232
|
+
const pipelineAsync = promisify(pipeline);
|
|
233
|
+
try {
|
|
234
|
+
// 将 ReadableStream 转换为 Node.js Readable
|
|
235
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
236
|
+
// 双向终止保护
|
|
237
|
+
req.on('close', () => nodeStream.destroy());
|
|
238
|
+
res.on('close', () => nodeStream.destroy());
|
|
239
|
+
await pipelineAsync(nodeStream, res);
|
|
240
|
+
return; // 成功完成管道传输后返回
|
|
241
|
+
}
|
|
242
|
+
catch (pipeError) {
|
|
243
|
+
console.error(`Stream pipeline error for ${relativePathPrefixedWithSlash}:`, pipeError);
|
|
244
|
+
if (!res.headersSent) {
|
|
245
|
+
res.writeHead(502).end('Stream Error');
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
222
249
|
}
|
|
223
250
|
}
|
|
224
251
|
}
|
|
225
252
|
catch (e) {
|
|
226
253
|
console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
|
|
227
|
-
// 继续尝试下一个注册表
|
|
228
254
|
}
|
|
229
255
|
finally {
|
|
256
|
+
// 不需要手动销毁 ReadableStream
|
|
230
257
|
limiter.release();
|
|
231
258
|
}
|
|
232
259
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import fetch, {Response} from 'node-fetch';
|
|
|
8
8
|
import {homedir} from 'os';
|
|
9
9
|
import {join, resolve} from 'path';
|
|
10
10
|
import {URL} from 'url';
|
|
11
|
+
import {Readable} from "node:stream";
|
|
11
12
|
|
|
12
13
|
const {readFile, writeFile} = fsPromises;
|
|
13
14
|
|
|
@@ -222,11 +223,12 @@ export async function startProxyServer(
|
|
|
222
223
|
// 修改为按顺序尝试注册表,找到第一个成功响应即返回
|
|
223
224
|
for (const {normalizedRegistryUrl, token} of registryInfos) {
|
|
224
225
|
await limiter.acquire();
|
|
226
|
+
let response: Response | null = null;
|
|
225
227
|
try {
|
|
226
228
|
const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
|
|
227
229
|
console.log(`Fetching from: ${targetUrl}`);
|
|
228
230
|
const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
|
|
229
|
-
|
|
231
|
+
response = await fetch(targetUrl, {headers});
|
|
230
232
|
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
231
233
|
|
|
232
234
|
if (response.ok) {
|
|
@@ -260,28 +262,64 @@ export async function startProxyServer(
|
|
|
260
262
|
// 继续尝试下一个注册表
|
|
261
263
|
}
|
|
262
264
|
} else {
|
|
263
|
-
// 非application/json则是tarball
|
|
264
265
|
if (!response.body) {
|
|
265
266
|
console.error(`Empty response body from ${response.url}, status: ${response.status}`);
|
|
266
|
-
// 继续尝试下一个注册表
|
|
267
267
|
continue;
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
// 设置正确的响应头
|
|
269
271
|
const contentLength = response.headers.get('Content-Length');
|
|
270
|
-
const safeHeaders: OutgoingHttpHeaders = {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
const safeHeaders: OutgoingHttpHeaders = {
|
|
273
|
+
'content-type': contentType,
|
|
274
|
+
'connection': 'keep-alive'
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (contentLength && !isNaN(Number(contentLength))) {
|
|
278
|
+
safeHeaders['content-length'] = contentLength;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 复制其他可能有用的头信息
|
|
282
|
+
const headersToCopy = ['cache-control', 'etag', 'last-modified'];
|
|
283
|
+
headersToCopy.forEach(header => {
|
|
284
|
+
const value = response?.headers.get(header);
|
|
285
|
+
if (value) {
|
|
286
|
+
safeHeaders[header] = value;
|
|
287
|
+
}
|
|
277
288
|
});
|
|
278
|
-
|
|
289
|
+
|
|
290
|
+
res.writeHead(response.status, safeHeaders);
|
|
291
|
+
|
|
292
|
+
// 使用 pipeline 处理流
|
|
293
|
+
const {pipeline} = await import('stream');
|
|
294
|
+
const {promisify} = await import('util');
|
|
295
|
+
const pipelineAsync = promisify(pipeline);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// 将 ReadableStream 转换为 Node.js Readable
|
|
299
|
+
const nodeStream = Readable.fromWeb(response.body as any);
|
|
300
|
+
|
|
301
|
+
// 双向终止保护
|
|
302
|
+
req.on('close', () => nodeStream.destroy());
|
|
303
|
+
res.on('close', () => nodeStream.destroy());
|
|
304
|
+
|
|
305
|
+
await pipelineAsync(
|
|
306
|
+
nodeStream,
|
|
307
|
+
res
|
|
308
|
+
);
|
|
309
|
+
return; // 成功完成管道传输后返回
|
|
310
|
+
} catch (pipeError) {
|
|
311
|
+
console.error(`Stream pipeline error for ${relativePathPrefixedWithSlash}:`, pipeError);
|
|
312
|
+
if (!res.headersSent) {
|
|
313
|
+
res.writeHead(502).end('Stream Error');
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
279
317
|
}
|
|
280
318
|
}
|
|
281
319
|
} catch (e) {
|
|
282
320
|
console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
|
|
283
|
-
// 继续尝试下一个注册表
|
|
284
321
|
} finally {
|
|
322
|
+
// 不需要手动销毁 ReadableStream
|
|
285
323
|
limiter.release();
|
|
286
324
|
}
|
|
287
325
|
}
|