com.jimuwd.xian.registry-proxy 1.0.54 → 1.0.56
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.d.ts +4 -0
- package/dist/index.js +18 -17
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +24 -0
- package/package.json +4 -1
- package/src/index.ts +0 -402
- package/tsconfig.json +0 -14
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server as HttpServer } from 'http';
|
|
3
|
+
import { Server as HttpsServer } from 'https';
|
|
4
|
+
export declare function startProxyServer(proxyConfigPath?: string, localYarnConfigPath?: string, globalYarnConfigPath?: string, port?: number): Promise<HttpServer | HttpsServer>;
|
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 logger from "./utils/logger";
|
|
10
11
|
const { readFile, writeFile } = fsPromises;
|
|
11
12
|
class ConcurrencyLimiter {
|
|
12
13
|
maxConcurrency;
|
|
@@ -84,13 +85,13 @@ async function readProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
|
|
|
84
85
|
const content = await readFile(resolvedPath, 'utf8');
|
|
85
86
|
const config = load(content);
|
|
86
87
|
if (!config.registries) {
|
|
87
|
-
|
|
88
|
+
logger.error('Missing required "registries" field in config');
|
|
88
89
|
process.exit(1);
|
|
89
90
|
}
|
|
90
91
|
return config;
|
|
91
92
|
}
|
|
92
93
|
catch (e) {
|
|
93
|
-
|
|
94
|
+
logger.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
94
95
|
process.exit(1);
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -100,7 +101,7 @@ async function readYarnConfig(path) {
|
|
|
100
101
|
return load(content);
|
|
101
102
|
}
|
|
102
103
|
catch (e) {
|
|
103
|
-
|
|
104
|
+
logger.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
104
105
|
return {};
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -140,16 +141,16 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
|
|
|
140
141
|
async function fetchFromRegistry(registry, targetUrl, limiter) {
|
|
141
142
|
await limiter.acquire();
|
|
142
143
|
try {
|
|
143
|
-
|
|
144
|
+
logger.info(`Fetching from: ${targetUrl}`);
|
|
144
145
|
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : {};
|
|
145
146
|
headers.Collection = "keep-alive";
|
|
146
147
|
const response = await fetch(targetUrl, { headers });
|
|
147
|
-
|
|
148
|
+
logger.info(`Response from upstream ${targetUrl}: ${response.status} ${response.statusText} content-type=${response.headers.get('content-type')} content-length=${response.headers.get('content-length')} transfer-encoding=${response.headers.get('transfer-encoding')}`);
|
|
148
149
|
return response.ok ? response : null;
|
|
149
150
|
}
|
|
150
151
|
catch (e) {
|
|
151
152
|
if (e instanceof Error) {
|
|
152
|
-
|
|
153
|
+
logger.error(e.code === 'ECONNREFUSED'
|
|
153
154
|
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
154
155
|
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`);
|
|
155
156
|
}
|
|
@@ -200,7 +201,7 @@ async function writeSuccessfulResponse(registryInfo, targetUrl, res, upstreamRes
|
|
|
200
201
|
else {
|
|
201
202
|
// 二进制流处理
|
|
202
203
|
if (!upstreamResponse.body) {
|
|
203
|
-
|
|
204
|
+
logger.error(`Empty response body from ${targetUrl}`);
|
|
204
205
|
res.writeHead(502).end('Empty Upstream Response');
|
|
205
206
|
}
|
|
206
207
|
else {
|
|
@@ -218,7 +219,7 @@ async function writeSuccessfulResponse(registryInfo, targetUrl, res, upstreamRes
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
catch (err) {
|
|
221
|
-
|
|
222
|
+
logger.error('Failed to write upstreamResponse:', err);
|
|
222
223
|
if (!res.headersSent) {
|
|
223
224
|
res.writeHead(502).end('Internal Server Error');
|
|
224
225
|
}
|
|
@@ -228,9 +229,9 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
228
229
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
229
230
|
const registryInfos = proxyInfo.registries;
|
|
230
231
|
const basePathPrefixedWithSlash = removeEndingSlashAndForceStartingSlash(proxyInfo.basePath);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
logger.info('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
|
|
233
|
+
logger.info('Proxy base path:', basePathPrefixedWithSlash);
|
|
234
|
+
logger.info('HTTPS:', !!proxyInfo.https);
|
|
234
235
|
let proxyPort;
|
|
235
236
|
const requestHandler = async (req, res) => {
|
|
236
237
|
if (!req.url || !req.headers.host) {
|
|
@@ -279,7 +280,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
279
280
|
await fsPromises.access(certPath);
|
|
280
281
|
}
|
|
281
282
|
catch (e) {
|
|
282
|
-
|
|
283
|
+
logger.error(`HTTPS config error: key or cert file not found`, e);
|
|
283
284
|
process.exit(1);
|
|
284
285
|
}
|
|
285
286
|
const httpsOptions = {
|
|
@@ -294,18 +295,18 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
294
295
|
const promisedServer = new Promise((resolve, reject) => {
|
|
295
296
|
server.on('error', (err) => {
|
|
296
297
|
if (err.code === 'EADDRINUSE') {
|
|
297
|
-
|
|
298
|
+
logger.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
298
299
|
process.exit(1);
|
|
299
300
|
}
|
|
300
|
-
|
|
301
|
+
logger.error('Server error:', err);
|
|
301
302
|
reject(err);
|
|
302
303
|
});
|
|
303
304
|
server.listen(port, () => {
|
|
304
305
|
const address = server.address();
|
|
305
306
|
proxyPort = address.port;
|
|
306
307
|
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
307
|
-
writeFile(portFile, proxyPort.toString()).catch(e =>
|
|
308
|
-
|
|
308
|
+
writeFile(portFile, proxyPort.toString()).catch(e => logger.error('Failed to write port file:', e));
|
|
309
|
+
logger.info(`Proxy server running on ${proxyInfo.https ? 'https' : 'http'}://localhost:${proxyPort}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`);
|
|
309
310
|
resolve(server);
|
|
310
311
|
});
|
|
311
312
|
});
|
|
@@ -314,7 +315,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
314
315
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
315
316
|
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
316
317
|
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
317
|
-
|
|
318
|
+
logger.error('Failed to start server:', err);
|
|
318
319
|
process.exit(1);
|
|
319
320
|
});
|
|
320
321
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// utils/logger.ts
|
|
2
|
+
const COLORS = {
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
proxy: '\x1b[36m', // 青色
|
|
5
|
+
success: '\x1b[32m', // 绿色
|
|
6
|
+
error: '\x1b[31m', // 红色
|
|
7
|
+
warn: '\x1b[33m', // 黄色
|
|
8
|
+
debug: '\x1b[35m' // 紫色
|
|
9
|
+
};
|
|
10
|
+
const PREFIX = {
|
|
11
|
+
proxy: `${COLORS.proxy}[PROXY]${COLORS.reset}`,
|
|
12
|
+
error: `${COLORS.error}[ERROR]${COLORS.reset}`,
|
|
13
|
+
warn: `${COLORS.warn}[WARN]${COLORS.reset}`,
|
|
14
|
+
debug: `${COLORS.debug}[DEBUG]${COLORS.reset}`
|
|
15
|
+
};
|
|
16
|
+
// 代理服务器专用日志
|
|
17
|
+
const log = {
|
|
18
|
+
info: (...args) => console.log(`${PREFIX.proxy}`, ...args),
|
|
19
|
+
success: (...args) => console.log(`${PREFIX.proxy} ${COLORS.success}✓${COLORS.reset}`, ...args),
|
|
20
|
+
error: (...args) => console.error(`${PREFIX.error}`, ...args),
|
|
21
|
+
warn: (...args) => console.warn(`${PREFIX.warn}`, ...args),
|
|
22
|
+
debug: (...args) => process.env.DEBUG && console.debug(`${PREFIX.debug}`, ...args)
|
|
23
|
+
};
|
|
24
|
+
export default log;
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.jimuwd.xian.registry-proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A lightweight npm registry proxy with fallback support",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"registry-proxy": "dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"scripts": {
|
|
11
14
|
"build": "tsc",
|
|
12
15
|
"deploy": "yarn build && yarn npm publish"
|
package/src/index.ts
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {createServer, IncomingMessage, OutgoingHttpHeaders, Server as HttpServer, ServerResponse} from 'http';
|
|
3
|
-
import {createServer as createHttpsServer, Server as HttpsServer} from 'https';
|
|
4
|
-
import {promises as fsPromises, readFileSync} from 'fs';
|
|
5
|
-
import {AddressInfo} from 'net';
|
|
6
|
-
import {load} from 'js-yaml';
|
|
7
|
-
import fetch, {Response} from 'node-fetch';
|
|
8
|
-
import {homedir} from 'os';
|
|
9
|
-
import {join, resolve} from 'path';
|
|
10
|
-
import {URL} from 'url';
|
|
11
|
-
|
|
12
|
-
const {readFile, writeFile} = fsPromises;
|
|
13
|
-
|
|
14
|
-
interface RegistryConfig {
|
|
15
|
-
npmAuthToken?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface HttpsConfig {
|
|
19
|
-
key: string;
|
|
20
|
-
cert: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ProxyConfig {
|
|
24
|
-
registries: Record<string, RegistryConfig | null>;
|
|
25
|
-
https?: HttpsConfig;
|
|
26
|
-
basePath?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface YarnConfig {
|
|
30
|
-
npmRegistries?: Record<string, RegistryConfig | null>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface RegistryInfo {
|
|
34
|
-
normalizedRegistryUrl: string;
|
|
35
|
-
token?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface ProxyInfo {
|
|
39
|
-
registries: RegistryInfo[];
|
|
40
|
-
https?: HttpsConfig;
|
|
41
|
-
basePath?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface PackageVersion {
|
|
45
|
-
dist?: { tarball?: string };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface PackageData {
|
|
49
|
-
versions?: Record<string, PackageVersion>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
class ConcurrencyLimiter {
|
|
53
|
-
private readonly maxConcurrency: number;
|
|
54
|
-
private current: number = 0;
|
|
55
|
-
private queue: Array<() => void> = [];
|
|
56
|
-
|
|
57
|
-
constructor(maxConcurrency: number) {
|
|
58
|
-
this.maxConcurrency = maxConcurrency;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async acquire(): Promise<void> {
|
|
62
|
-
if (this.current < this.maxConcurrency) {
|
|
63
|
-
this.current++;
|
|
64
|
-
return Promise.resolve();
|
|
65
|
-
}
|
|
66
|
-
return new Promise((resolve) => {
|
|
67
|
-
this.queue.push(resolve);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
release(): void {
|
|
72
|
-
this.current--;
|
|
73
|
-
const next = this.queue.shift();
|
|
74
|
-
if (next) {
|
|
75
|
-
this.current++;
|
|
76
|
-
next();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const limiter = new ConcurrencyLimiter(10);
|
|
82
|
-
|
|
83
|
-
function removeEndingSlashAndForceStartingSlash(str: string | undefined | null): string {
|
|
84
|
-
if (!str) return '/';
|
|
85
|
-
let trimmed = str.trim();
|
|
86
|
-
if (trimmed === '/') return '/';
|
|
87
|
-
if (trimmed === '') return '/';
|
|
88
|
-
if (!trimmed.startsWith('/')) trimmed = '/' + trimmed;
|
|
89
|
-
return trimmed.replace(/\/+$/, '');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function normalizeUrl(httpOrHttpsUrl: string): string {
|
|
93
|
-
if (!httpOrHttpsUrl.startsWith("http")) throw new Error("http(s) url must starts with 'http(s)://'");
|
|
94
|
-
try {
|
|
95
|
-
const urlObj = new URL(httpOrHttpsUrl);
|
|
96
|
-
if (urlObj.protocol === 'http:' && (urlObj.port === '80' || urlObj.port === '')) {
|
|
97
|
-
urlObj.port = '';
|
|
98
|
-
} else if (urlObj.protocol === 'https:' && (urlObj.port === '443' || urlObj.port === '')) {
|
|
99
|
-
urlObj.port = '';
|
|
100
|
-
}
|
|
101
|
-
return urlObj.toString().replace(/\/+$/, '');
|
|
102
|
-
} catch (e) {
|
|
103
|
-
throw new Error(`Invalid URL: ${httpOrHttpsUrl}`, {cause: e});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function resolvePath(path: string): string {
|
|
108
|
-
return path.startsWith('~/') ? join(homedir(), path.slice(2)) : resolve(path);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
|
|
112
|
-
const normalizedTarball = normalizeUrl(tarballUrl);
|
|
113
|
-
const normalizedRegistries = registries
|
|
114
|
-
.map(r => normalizeUrl(r.normalizedRegistryUrl))
|
|
115
|
-
.sort((a, b) => b.length - a.length);
|
|
116
|
-
for (const normalizedRegistry of normalizedRegistries) {
|
|
117
|
-
if (normalizedTarball.startsWith(normalizedRegistry)) {
|
|
118
|
-
return normalizedTarball.slice(normalizedRegistry.length) || '/';
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
throw new Error(`Can't find tarball url ${tarballUrl} does not match given registries ${normalizedRegistries}`)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function readProxyConfig(proxyConfigPath = './.registry-proxy.yml'): Promise<ProxyConfig> {
|
|
125
|
-
const resolvedPath = resolvePath(proxyConfigPath);
|
|
126
|
-
try {
|
|
127
|
-
const content = await readFile(resolvedPath, 'utf8');
|
|
128
|
-
const config = load(content) as ProxyConfig;
|
|
129
|
-
if (!config.registries) {
|
|
130
|
-
console.error('Missing required "registries" field in config');
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
return config;
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function readYarnConfig(path: string): Promise<YarnConfig> {
|
|
141
|
-
try {
|
|
142
|
-
const content = await readFile(resolvePath(path), 'utf8');
|
|
143
|
-
return load(content) as YarnConfig;
|
|
144
|
-
} catch (e) {
|
|
145
|
-
console.warn(`Failed to load Yarn config from ${path}:`, e);
|
|
146
|
-
return {};
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function loadProxyInfo(
|
|
151
|
-
proxyConfigPath = './.registry-proxy.yml',
|
|
152
|
-
localYarnConfigPath = './.yarnrc.yml',
|
|
153
|
-
globalYarnConfigPath = join(homedir(), '.yarnrc.yml')
|
|
154
|
-
): Promise<ProxyInfo> {
|
|
155
|
-
const [proxyConfig, localYarnConfig, globalYarnConfig] = await Promise.all([
|
|
156
|
-
readProxyConfig(proxyConfigPath),
|
|
157
|
-
readYarnConfig(localYarnConfigPath),
|
|
158
|
-
readYarnConfig(globalYarnConfigPath)
|
|
159
|
-
]);
|
|
160
|
-
const registryMap = new Map<string, RegistryInfo>();
|
|
161
|
-
for (const [proxiedRegUrl, proxyRegConfig] of Object.entries(proxyConfig.registries)) {
|
|
162
|
-
const normalizedProxiedRegUrl = normalizeUrl(proxiedRegUrl);
|
|
163
|
-
let token = proxyRegConfig?.npmAuthToken;
|
|
164
|
-
if (!token) {
|
|
165
|
-
const yarnConfigs = [localYarnConfig, globalYarnConfig];
|
|
166
|
-
for (const yarnConfig of yarnConfigs) {
|
|
167
|
-
if (yarnConfig.npmRegistries) {
|
|
168
|
-
const foundEntry = Object.entries(yarnConfig.npmRegistries)
|
|
169
|
-
.find(([registryUrl]) => normalizedProxiedRegUrl === normalizeUrl(registryUrl))
|
|
170
|
-
if (foundEntry) {
|
|
171
|
-
const [, registryConfig] = foundEntry;
|
|
172
|
-
if (registryConfig?.npmAuthToken) {
|
|
173
|
-
token = registryConfig.npmAuthToken;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
registryMap.set(normalizedProxiedRegUrl, {normalizedRegistryUrl: normalizedProxiedRegUrl, token});
|
|
181
|
-
}
|
|
182
|
-
const registries = Array.from(registryMap.values());
|
|
183
|
-
const https = proxyConfig.https;
|
|
184
|
-
const basePath = removeEndingSlashAndForceStartingSlash(proxyConfig.basePath);
|
|
185
|
-
return {registries, https, basePath};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function fetchFromRegistry(
|
|
189
|
-
registry: RegistryInfo,
|
|
190
|
-
targetUrl: string,
|
|
191
|
-
limiter: ConcurrencyLimiter
|
|
192
|
-
): Promise<Response | null> {
|
|
193
|
-
await limiter.acquire();
|
|
194
|
-
try {
|
|
195
|
-
console.log(`Fetching from: ${targetUrl}`);
|
|
196
|
-
const headers: {} = registry.token ? {Authorization: `Bearer ${registry.token}`} : {};
|
|
197
|
-
(headers as any).Collection = "keep-alive";
|
|
198
|
-
const response = await fetch(targetUrl, {headers});
|
|
199
|
-
console.log(`Response from upstream ${targetUrl}: ${response.status} ${response.statusText} content-type=${response.headers.get('content-type')} content-length=${response.headers.get('content-length')} transfer-encoding=${response.headers.get('transfer-encoding')}`);
|
|
200
|
-
return response.ok ? response : null;
|
|
201
|
-
} catch (e) {
|
|
202
|
-
if (e instanceof Error) {
|
|
203
|
-
console.error(
|
|
204
|
-
(e as any).code === 'ECONNREFUSED'
|
|
205
|
-
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
206
|
-
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return null;
|
|
210
|
-
} finally {
|
|
211
|
-
limiter.release();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 修改后的 writeSuccessfulResponse 函数
|
|
216
|
-
async function writeSuccessfulResponse(
|
|
217
|
-
registryInfo: RegistryInfo,
|
|
218
|
-
targetUrl: string,
|
|
219
|
-
res: ServerResponse,
|
|
220
|
-
upstreamResponse: Response,
|
|
221
|
-
req: IncomingMessage,
|
|
222
|
-
proxyInfo: ProxyInfo,
|
|
223
|
-
proxyPort: number,
|
|
224
|
-
registryInfos: RegistryInfo[]
|
|
225
|
-
): Promise<void> {
|
|
226
|
-
|
|
227
|
-
if (!upstreamResponse.ok) throw new Error("Only 2xx upstream response is supported");
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const contentType = upstreamResponse.headers.get('content-type') || 'application/octet-stream';
|
|
231
|
-
const connection = upstreamResponse.headers.get("connection") || 'keep-alive';
|
|
232
|
-
|
|
233
|
-
// 准备通用头信息
|
|
234
|
-
const safeHeaders: OutgoingHttpHeaders = {'connection': connection, 'content-type': contentType,};
|
|
235
|
-
|
|
236
|
-
// 复制所有可能需要的头信息
|
|
237
|
-
const headersToCopy = [
|
|
238
|
-
'content-length',
|
|
239
|
-
'content-encoding',
|
|
240
|
-
'transfer-encoding',
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
headersToCopy.forEach(header => {
|
|
244
|
-
const value = upstreamResponse.headers.get(header);
|
|
245
|
-
if (value) safeHeaders[header] = value;
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
if (contentType.includes('application/json')) {
|
|
249
|
-
// JSON 处理逻辑
|
|
250
|
-
const data = await upstreamResponse.json() as PackageData;
|
|
251
|
-
if (data.versions) {
|
|
252
|
-
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
253
|
-
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
254
|
-
|
|
255
|
-
for (const versionKey in data.versions) {
|
|
256
|
-
const packageVersion = data.versions[versionKey];
|
|
257
|
-
const tarball = packageVersion?.dist?.tarball;
|
|
258
|
-
if (tarball) {
|
|
259
|
-
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
260
|
-
const proxiedTarballUrl: string = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
261
|
-
packageVersion!.dist!.tarball = proxiedTarballUrl as string;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
res.writeHead(upstreamResponse.status, {"content-type": contentType}).end(JSON.stringify(data));
|
|
266
|
-
} else {
|
|
267
|
-
// 二进制流处理
|
|
268
|
-
if (!upstreamResponse.body) {
|
|
269
|
-
console.error(`Empty response body from ${targetUrl}`);
|
|
270
|
-
res.writeHead(502).end('Empty Upstream Response');
|
|
271
|
-
} else {
|
|
272
|
-
// write back to client
|
|
273
|
-
res.writeHead(upstreamResponse.status, safeHeaders);
|
|
274
|
-
// stop transfer if client is closed accidentally.
|
|
275
|
-
// req.on('close', () => upstreamResponse.body?.unpipe());
|
|
276
|
-
// write back body data (chunked probably)
|
|
277
|
-
upstreamResponse.body
|
|
278
|
-
.on('data', (chunk) => res.write(chunk))
|
|
279
|
-
.on('end', () => res.end())
|
|
280
|
-
.on('close', () => res.destroy(new Error(`Upstream server ${registryInfo.normalizedRegistryUrl} closed connection while transferring data on url ${targetUrl}`)))
|
|
281
|
-
.on('error', (err: Error) => res.destroy(new Error(`Stream error: ${err.message}`, {cause: err,})));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} catch (err) {
|
|
285
|
-
console.error('Failed to write upstreamResponse:', err);
|
|
286
|
-
if (!res.headersSent) {
|
|
287
|
-
res.writeHead(502).end('Internal Server Error');
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export async function startProxyServer(
|
|
293
|
-
proxyConfigPath?: string,
|
|
294
|
-
localYarnConfigPath?: string,
|
|
295
|
-
globalYarnConfigPath?: string,
|
|
296
|
-
port: number = 0
|
|
297
|
-
): Promise<HttpServer | HttpsServer> {
|
|
298
|
-
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
299
|
-
const registryInfos = proxyInfo.registries;
|
|
300
|
-
const basePathPrefixedWithSlash: string = removeEndingSlashAndForceStartingSlash(proxyInfo.basePath);
|
|
301
|
-
|
|
302
|
-
console.log('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
|
|
303
|
-
console.log('Proxy base path:', basePathPrefixedWithSlash);
|
|
304
|
-
console.log('HTTPS:', !!proxyInfo.https);
|
|
305
|
-
|
|
306
|
-
let proxyPort: number;
|
|
307
|
-
|
|
308
|
-
const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
|
|
309
|
-
if (!req.url || !req.headers.host) {
|
|
310
|
-
res.writeHead(400).end('Invalid Request');
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
315
|
-
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
316
|
-
res.writeHead(404).end('Not Found');
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const path = basePathPrefixedWithSlash === '/'
|
|
321
|
-
? fullUrl.pathname
|
|
322
|
-
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
323
|
-
|
|
324
|
-
// 顺序尝试注册表,获取第一个成功响应
|
|
325
|
-
let successfulResponse: Response | null = null;
|
|
326
|
-
let targetRegistry: RegistryInfo | null = null;
|
|
327
|
-
let targetUrl: string | null = null;
|
|
328
|
-
for (const registry of registryInfos) {
|
|
329
|
-
if (req.destroyed) break;
|
|
330
|
-
targetRegistry = registry;
|
|
331
|
-
const search = fullUrl.search || '';
|
|
332
|
-
targetUrl = `${registry.normalizedRegistryUrl}${path}${search}`;
|
|
333
|
-
const okResponseOrNull = await fetchFromRegistry(registry, targetUrl, limiter);
|
|
334
|
-
if (okResponseOrNull) {
|
|
335
|
-
successfulResponse = okResponseOrNull;
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// 统一回写响应
|
|
341
|
-
if (successfulResponse) {
|
|
342
|
-
await writeSuccessfulResponse(targetRegistry!, targetUrl!, res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
343
|
-
} else {
|
|
344
|
-
res.writeHead(404).end('All upstream registries failed');
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
let server: HttpServer | HttpsServer;
|
|
349
|
-
if (proxyInfo.https) {
|
|
350
|
-
const {key, cert} = proxyInfo.https;
|
|
351
|
-
const keyPath = resolvePath(key);
|
|
352
|
-
const certPath = resolvePath(cert);
|
|
353
|
-
try {
|
|
354
|
-
await fsPromises.access(keyPath);
|
|
355
|
-
await fsPromises.access(certPath);
|
|
356
|
-
} catch (e) {
|
|
357
|
-
console.error(`HTTPS config error: key or cert file not found`, e);
|
|
358
|
-
process.exit(1);
|
|
359
|
-
}
|
|
360
|
-
const httpsOptions = {
|
|
361
|
-
key: readFileSync(keyPath),
|
|
362
|
-
cert: readFileSync(certPath),
|
|
363
|
-
};
|
|
364
|
-
server = createHttpsServer(httpsOptions, requestHandler);
|
|
365
|
-
} else {
|
|
366
|
-
server = createServer(requestHandler);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const promisedServer: Promise<HttpServer | HttpsServer> = new Promise((resolve, reject) => {
|
|
370
|
-
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
371
|
-
if (err.code === 'EADDRINUSE') {
|
|
372
|
-
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
373
|
-
process.exit(1);
|
|
374
|
-
}
|
|
375
|
-
console.error('Server error:', err);
|
|
376
|
-
reject(err);
|
|
377
|
-
});
|
|
378
|
-
server.listen(port, () => {
|
|
379
|
-
const address = server.address() as AddressInfo;
|
|
380
|
-
proxyPort = address.port;
|
|
381
|
-
const portFile = join(process.env.PROJECT_ROOT || process.cwd(), '.registry-proxy-port');
|
|
382
|
-
writeFile(portFile, proxyPort.toString()).catch(e => console.error('Failed to write port file:', e));
|
|
383
|
-
console.log(`Proxy server running on ${proxyInfo.https ? 'https' : 'http'}://localhost:${proxyPort}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`);
|
|
384
|
-
resolve(server);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
return promisedServer as Promise<HttpServer | HttpsServer>;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
392
|
-
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
393
|
-
startProxyServer(
|
|
394
|
-
configPath,
|
|
395
|
-
localYarnPath,
|
|
396
|
-
globalYarnPath,
|
|
397
|
-
parseInt(port, 10) || 0
|
|
398
|
-
).catch(err => {
|
|
399
|
-
console.error('Failed to start server:', err);
|
|
400
|
-
process.exit(1);
|
|
401
|
-
});
|
|
402
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "esnext",
|
|
5
|
-
"moduleResolution": "node",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"]
|
|
14
|
-
}
|