xshell 1.0.48 → 1.0.50
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/file.d.ts +8 -1
- package/file.js +69 -5
- package/i18n/dict.json +9 -0
- package/i18n/rwdict.js +2 -2
- package/net.browser.js +1 -0
- package/net.js +4 -1
- package/package.json +12 -11
- package/prototype.js +2 -1
- package/repl.js +2 -9
- package/server.d.ts +21 -9
- package/server.js +93 -55
- package/utils.browser.d.ts +3 -0
- package/utils.browser.js +8 -0
- package/utils.d.ts +3 -0
- package/utils.js +10 -2
package/file.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare function fexists(fp: string, { print }?: {
|
|
|
21
21
|
- flags: `'r'`
|
|
22
22
|
- options?:
|
|
23
23
|
- mode?: `'0o666'` Sets the file mode (permission and sticky bits) if the file is created. */
|
|
24
|
-
export declare function fopen(fp: string, flags
|
|
24
|
+
export declare function fopen(fp: string, flags?: string | number, { mode, print }?: {
|
|
25
25
|
mode?: fs.Mode;
|
|
26
26
|
print?: boolean;
|
|
27
27
|
}): Promise<fsp.FileHandle & {
|
|
@@ -91,6 +91,8 @@ export declare function flist(fpd: string, options?: FListOptions & {
|
|
|
91
91
|
}): Promise<FStats[]>;
|
|
92
92
|
export declare function flist(fpd: string, options?: FListOptions): Promise<string[]>;
|
|
93
93
|
export declare function fstat(fp: string): Promise<FStats>;
|
|
94
|
+
export declare function flstat(fp: string): Promise<FStats>;
|
|
95
|
+
export declare function ffstat(handle: fsp.FileHandle): Promise<fs.BigIntStats>;
|
|
94
96
|
/** 删除文件或文件夹 delete files or folders
|
|
95
97
|
- fp: 文件或文件夹的完整路径 The full path to the file or folder
|
|
96
98
|
- options?:
|
|
@@ -155,6 +157,11 @@ export declare function flink(fp_real: string, fp_link: string, { junction, prin
|
|
|
155
157
|
junction?: boolean;
|
|
156
158
|
print?: boolean;
|
|
157
159
|
}): Promise<void>;
|
|
160
|
+
export declare let fwatchers: Record<string, fs.FSWatcher>;
|
|
161
|
+
/** 跟踪文本文件追加的内容,类似 tail -f */
|
|
162
|
+
export declare function ftail(fp: string, handler: (lines: string[]) => void | Promise<void>, { print }?: {
|
|
163
|
+
print?: boolean;
|
|
164
|
+
}): Promise<fs.FSWatcher>;
|
|
158
165
|
/** 打开一个文件并搜索替换某个 pattern */
|
|
159
166
|
export declare function freplace(fp: string, pattern: string | RegExp, replacement: string, { print }?: {
|
|
160
167
|
print?: boolean;
|
package/file.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isUint8Array } from 'util/types';
|
|
|
3
3
|
import { path } from './path.js';
|
|
4
4
|
import { t } from './i18n/instance.js';
|
|
5
5
|
import { to_json } from './prototype.js';
|
|
6
|
-
import { assert } from './utils.js';
|
|
6
|
+
import { assert, Lock } from './utils.js';
|
|
7
7
|
export const encodings = ['utf-8', 'gb18030', 'shift-jis', 'utf-16le'];
|
|
8
8
|
/** fp 所指向的 文件/ 文件夹 是否存在
|
|
9
9
|
Does the file/folder pointed to by fp exist? */
|
|
@@ -22,7 +22,7 @@ export function fexists(fp, { print = true } = {}) {
|
|
|
22
22
|
- flags: `'r'`
|
|
23
23
|
- options?:
|
|
24
24
|
- mode?: `'0o666'` Sets the file mode (permission and sticky bits) if the file is created. */
|
|
25
|
-
export async function fopen(fp, flags, { mode, print } = {}) {
|
|
25
|
+
export async function fopen(fp, flags = 'r', { mode, print } = {}) {
|
|
26
26
|
if (print)
|
|
27
27
|
console.log(t('打开文件'), fp);
|
|
28
28
|
return Object.assign(await fsp.open(fp, flags, mode), { fp, flags, mode });
|
|
@@ -163,6 +163,22 @@ export async function fstat(fp) {
|
|
|
163
163
|
stat.fp = fp;
|
|
164
164
|
return stat;
|
|
165
165
|
}
|
|
166
|
+
export async function flstat(fp) {
|
|
167
|
+
assert(path.isAbsolute(fp), t("flstat: 参数 fp: '{{fp}}' 必须是绝对路径", { fp }));
|
|
168
|
+
let stat = await fsp.lstat(fp, { bigint: true });
|
|
169
|
+
stat.fp = fp;
|
|
170
|
+
return stat;
|
|
171
|
+
}
|
|
172
|
+
export async function ffstat(handle) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
fs.fstat(handle.fd, { bigint: true }, (error, stats) => {
|
|
175
|
+
if (error)
|
|
176
|
+
reject(error);
|
|
177
|
+
else
|
|
178
|
+
resolve(stats);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
166
182
|
/** 删除文件或文件夹 delete files or folders
|
|
167
183
|
- fp: 文件或文件夹的完整路径 The full path to the file or folder
|
|
168
184
|
- options?:
|
|
@@ -207,7 +223,7 @@ export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, }
|
|
|
207
223
|
assert(fp_src.isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
|
|
208
224
|
assert(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
|
|
209
225
|
if (print)
|
|
210
|
-
console.log(t('复制'), fp_src, '
|
|
226
|
+
console.log(t('复制'), fp_src, '->', fp_dst);
|
|
211
227
|
const { copy } = await import('fs-extra');
|
|
212
228
|
await copy(fp_src, fp_dst, { overwrite, errorOnExist: true });
|
|
213
229
|
}
|
|
@@ -222,7 +238,7 @@ export async function fmove(src, dst, { overwrite = false, print = true } = {})
|
|
|
222
238
|
if (!path.isAbsolute(src) || !path.isAbsolute(dst))
|
|
223
239
|
throw new Error(t('src 和 dst 必须为完整路径'));
|
|
224
240
|
if (print)
|
|
225
|
-
console.log(t('移动'), src, '
|
|
241
|
+
console.log(t('移动'), src, '->', dst);
|
|
226
242
|
const { move } = await import('fs-extra');
|
|
227
243
|
await move(src, dst, { overwrite });
|
|
228
244
|
}
|
|
@@ -241,7 +257,7 @@ export async function frename(fp, fp_, { fpd, print = true, overwrite = true } =
|
|
|
241
257
|
}
|
|
242
258
|
assert(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
|
|
243
259
|
if (print)
|
|
244
|
-
console.log(t('重命名'), fp, '
|
|
260
|
+
console.log(t('重命名'), fp, '->', fp_);
|
|
245
261
|
if (!overwrite && fexists(fp_))
|
|
246
262
|
throw new Error(t('文件已存在:') + fp_);
|
|
247
263
|
await fsp.rename(fp, fp_);
|
|
@@ -294,6 +310,54 @@ export async function flink(fp_real, fp_link, { junction = false, print = true }
|
|
|
294
310
|
else
|
|
295
311
|
fsp.symlink(fp_real, fp_link, is_fpd_real ? 'dir' : 'file');
|
|
296
312
|
}
|
|
313
|
+
export let fwatchers = {};
|
|
314
|
+
/** 跟踪文本文件追加的内容,类似 tail -f */
|
|
315
|
+
export async function ftail(fp, handler, { print = true } = {}) {
|
|
316
|
+
fwatchers[fp]?.close();
|
|
317
|
+
const { size } = await fstat(fp);
|
|
318
|
+
let pointer = Number(size);
|
|
319
|
+
let lock = new Lock(await fopen(fp));
|
|
320
|
+
let fbuf = new Uint8Array(2 ** 20);
|
|
321
|
+
let strbuf = '';
|
|
322
|
+
let decoder = new TextDecoder();
|
|
323
|
+
if (print)
|
|
324
|
+
console.log('开始跟踪追加内容', fp);
|
|
325
|
+
const { default: throttle } = await import('lodash/throttle.js');
|
|
326
|
+
const onchange_throttled = throttle(async () => {
|
|
327
|
+
if (lock.locked)
|
|
328
|
+
return;
|
|
329
|
+
await lock.request(async (handle) => {
|
|
330
|
+
const { bytesRead } = await handle.read(fbuf, 0, fbuf.length, pointer);
|
|
331
|
+
pointer += bytesRead;
|
|
332
|
+
const chunk = decoder.decode(fbuf.subarray(0, bytesRead), { stream: true });
|
|
333
|
+
let lines = [];
|
|
334
|
+
let j = 0;
|
|
335
|
+
for (let i = 0; (i = chunk.indexOf('\n', j)) >= 0;) {
|
|
336
|
+
let line = chunk.slice(j, i);
|
|
337
|
+
if (strbuf) {
|
|
338
|
+
line = strbuf + line;
|
|
339
|
+
strbuf = '';
|
|
340
|
+
}
|
|
341
|
+
j = i + 1;
|
|
342
|
+
lines.push(line);
|
|
343
|
+
}
|
|
344
|
+
strbuf = chunk.slice(j);
|
|
345
|
+
await handler(lines);
|
|
346
|
+
});
|
|
347
|
+
}, 250);
|
|
348
|
+
let watcher = fs.watch(fp, event => {
|
|
349
|
+
if (event === 'change')
|
|
350
|
+
onchange_throttled();
|
|
351
|
+
else {
|
|
352
|
+
console.error(`被监听的文件 ${fp.quote()} 出现了 rename 事件,结束 ftail`);
|
|
353
|
+
watcher.close();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
watcher.on('error', error => {
|
|
357
|
+
console.error(error);
|
|
358
|
+
});
|
|
359
|
+
return fwatchers[fp] = watcher;
|
|
360
|
+
}
|
|
297
361
|
/** 打开一个文件并搜索替换某个 pattern */
|
|
298
362
|
export async function freplace(fp, pattern, replacement, { print = true } = {}) {
|
|
299
363
|
await fwrite(fp, (await fread(fp, { print }))
|
package/i18n/dict.json
CHANGED
|
@@ -337,5 +337,14 @@
|
|
|
337
337
|
},
|
|
338
338
|
"fsend 必须传 absolute 选项或 root 文件夹": {
|
|
339
339
|
"en": "fsend must pass absolute option or root folder"
|
|
340
|
+
},
|
|
341
|
+
"message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null": {
|
|
342
|
+
"en": "There cannot be undefined items in the message.data array, because json will become null after serialization"
|
|
343
|
+
},
|
|
344
|
+
"xshell 启动成功,正在监听: http://localhost:8421": {
|
|
345
|
+
"en": "xshell started successfully and is listening: http://localhost:8421"
|
|
346
|
+
},
|
|
347
|
+
"flstat: 参数 fp: '{{fp}}' 必须是绝对路径": {
|
|
348
|
+
"en": "flstat: parameter fp: '{{fp}}' must be an absolute path"
|
|
340
349
|
}
|
|
341
350
|
}
|
package/i18n/rwdict.js
CHANGED
|
@@ -60,14 +60,14 @@ export class RWDict extends Dict {
|
|
|
60
60
|
if (_translation !== translation)
|
|
61
61
|
if (!overwrite) {
|
|
62
62
|
console.error(`${`已存在 ${id} 词条:`.red} ${JSON.stringify(item)}`);
|
|
63
|
-
console.error(`${'M? '.yellow}${_translation.replace(/\n/g, '\\n')}
|
|
63
|
+
console.error(`${'M? '.yellow}${_translation.replace(/\n/g, '\\n')} -> ${translation.replace(/\n/g, '\\n')}`);
|
|
64
64
|
if (!dryrun)
|
|
65
65
|
console.error('如要更新翻译请设置 { overwrite: true },否则使用 i18n.t(\'text\', { context: \'xxx\' }) 标记文本以区分。\n');
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
if (print)
|
|
70
|
-
console.log(`${'M '.yellow}${_translation.replace(/\n/g, '\\n')}
|
|
70
|
+
console.log(`${'M '.yellow}${_translation.replace(/\n/g, '\\n')} -> ${translation.replace(/\n/g, '\\n')}`);
|
|
71
71
|
if (!dryrun)
|
|
72
72
|
item[language] = translation;
|
|
73
73
|
}
|
package/net.browser.js
CHANGED
|
@@ -380,6 +380,7 @@ export class Remote {
|
|
|
380
380
|
作为 websocket 连接接收方,必传 websocket 参数
|
|
381
381
|
发送或连接出错时自动清理 message.id 对应的 handler */
|
|
382
382
|
async send(message, websocket) {
|
|
383
|
+
assert(!message.data || message.data.every(arg => arg !== undefined), t('message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null'));
|
|
383
384
|
if (this.print)
|
|
384
385
|
console.log('remote.send:', message);
|
|
385
386
|
try {
|
package/net.js
CHANGED
|
@@ -87,7 +87,9 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
87
87
|
// --- headers, http/2 开始都用小写的 headers
|
|
88
88
|
let headers = new Headers({
|
|
89
89
|
'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja-JP;q=0.6,ja;q=0.5',
|
|
90
|
-
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
90
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
|
91
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
92
|
+
'sec-ch-ua-platform-version': '"15.0.0"',
|
|
91
93
|
});
|
|
92
94
|
if (body !== undefined)
|
|
93
95
|
headers.set('content-type', type);
|
|
@@ -541,6 +543,7 @@ export class Remote {
|
|
|
541
543
|
作为 websocket 连接接收方,必传 websocket 参数
|
|
542
544
|
发送或连接出错时自动清理 message.id 对应的 handler */
|
|
543
545
|
async send(message, websocket) {
|
|
546
|
+
assert(!message.data || message.data.every(arg => arg !== undefined), t('message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null'));
|
|
544
547
|
if (this.print)
|
|
545
548
|
console.log('remote.send:', message);
|
|
546
549
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"interactive programming"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=20.5.
|
|
19
|
+
"node": ">=20.5.1",
|
|
20
20
|
"vscode": ">=1.79.0"
|
|
21
21
|
},
|
|
22
22
|
"author": "ShenHongFei <shen.hongfei@outlook.com> (https://github.com/ShenHongFei)",
|
|
@@ -67,19 +67,19 @@
|
|
|
67
67
|
"js-cookie": "^3.0.5",
|
|
68
68
|
"koa": "^2.14.2",
|
|
69
69
|
"koa-compress": "^5.1.1",
|
|
70
|
-
"koa-useragent": "^4.1.0",
|
|
71
70
|
"lodash": "^4.17.21",
|
|
72
71
|
"map-stream": "0.0.7",
|
|
73
72
|
"mime-types": "^2.1.35",
|
|
74
73
|
"ora": "^7.0.1",
|
|
75
74
|
"react": "^18.2.0",
|
|
76
|
-
"react-i18next": "^13.1.
|
|
75
|
+
"react-i18next": "^13.1.2",
|
|
77
76
|
"resolve-path": "^1.4.0",
|
|
78
77
|
"strip-ansi": "^7.1.0",
|
|
79
78
|
"through2": "^4.0.2",
|
|
80
79
|
"tough-cookie": "^4.1.3",
|
|
81
|
-
"tslib": "^2.6.
|
|
80
|
+
"tslib": "^2.6.2",
|
|
82
81
|
"typescript": "^5.1.6",
|
|
82
|
+
"ua-parser-js": "2.0.0-alpha.2",
|
|
83
83
|
"undici": "^5.23.0",
|
|
84
84
|
"vinyl": "^3.0.0",
|
|
85
85
|
"vinyl-fs": "^4.0.0",
|
|
@@ -97,17 +97,18 @@
|
|
|
97
97
|
"@types/koa-compress": "^4.0.3",
|
|
98
98
|
"@types/lodash": "^4.14.197",
|
|
99
99
|
"@types/mime-types": "^2.1.1",
|
|
100
|
-
"@types/node": "^20.
|
|
100
|
+
"@types/node": "^20.5.1",
|
|
101
101
|
"@types/react": "^18.2.20",
|
|
102
102
|
"@types/through2": "^2.0.38",
|
|
103
103
|
"@types/tough-cookie": "^4.0.2",
|
|
104
|
+
"@types/ua-parser-js": "^0.7.36",
|
|
104
105
|
"@types/vinyl-fs": "^3.0.2",
|
|
105
106
|
"@types/vscode": "^1.81.0",
|
|
106
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
107
|
-
"@typescript-eslint/parser": "^6.
|
|
108
|
-
"eslint": "^8.
|
|
109
|
-
"eslint-plugin-react": "^7.33.
|
|
110
|
-
"eslint-plugin-xlint": "^1.0.
|
|
107
|
+
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
|
108
|
+
"@typescript-eslint/parser": "^6.4.0",
|
|
109
|
+
"eslint": "^8.47.0",
|
|
110
|
+
"eslint-plugin-react": "^7.33.2",
|
|
111
|
+
"eslint-plugin-xlint": "^1.0.7"
|
|
111
112
|
},
|
|
112
113
|
"scripts": {
|
|
113
114
|
"start": "node --title=xshell --inspect=0.0.0.0:8420 ./xshell.js",
|
package/prototype.js
CHANGED
|
@@ -93,7 +93,8 @@ if (!globalThis.my_prototype_defined) {
|
|
|
93
93
|
cur_width += w;
|
|
94
94
|
if (cur_width > width) {
|
|
95
95
|
const i_fitted_next = i_fitted + 1;
|
|
96
|
-
|
|
96
|
+
// … 在 winterm 中对不齐,使用 ··· 代替
|
|
97
|
+
const t = s.slice(0, i_fitted_next) + ' '.repeat(width - 2 - fitted_width) + '··';
|
|
97
98
|
return color_bak ? color_bak + t + '\u001b[39m' : t;
|
|
98
99
|
}
|
|
99
100
|
}
|
package/repl.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { path } from './path.js';
|
|
5
5
|
import { t } from './i18n/instance.js';
|
|
6
6
|
import './prototype.js';
|
|
7
|
-
import { delay, set_inspect_options
|
|
7
|
+
import { delay, set_inspect_options } from './utils.js';
|
|
8
8
|
import { Remote } from './net.js';
|
|
9
9
|
set_inspect_options();
|
|
10
10
|
let server;
|
|
@@ -12,7 +12,6 @@ let server;
|
|
|
12
12
|
export const fpd_root = path.dirname(fileURLToPath(import.meta.url)) + '/';
|
|
13
13
|
export async function start_repl() {
|
|
14
14
|
// ------------ 加载库
|
|
15
|
-
console.log(t('xshell 开始启动').yellow);
|
|
16
15
|
// --- prevent from exiting
|
|
17
16
|
process.on('uncaughtException', error => {
|
|
18
17
|
console.error(error);
|
|
@@ -25,7 +24,6 @@ export async function start_repl() {
|
|
|
25
24
|
useColors: true,
|
|
26
25
|
terminal: true,
|
|
27
26
|
});
|
|
28
|
-
console.log(t('nodejs.repl 启动成功'));
|
|
29
27
|
process.title = 'xshell';
|
|
30
28
|
await Promise.all([
|
|
31
29
|
pollute_global(),
|
|
@@ -44,13 +42,9 @@ export async function start_repl() {
|
|
|
44
42
|
})
|
|
45
43
|
});
|
|
46
44
|
await server.start();
|
|
47
|
-
console.log(t('server 启动成功'));
|
|
48
45
|
})(),
|
|
49
46
|
]);
|
|
50
|
-
console.log(('
|
|
51
|
-
t('xshell 启动成功,用时 ') + delta2str(new Date().getTime() - globalThis.tstarted.getTime()) + '\n' +
|
|
52
|
-
t('正在监听: http://localhost:8421\n') +
|
|
53
|
-
'-'.repeat(30)).green);
|
|
47
|
+
console.log(t('xshell 启动成功,正在监听: http://localhost:8421').green);
|
|
54
48
|
}
|
|
55
49
|
export async function stop() {
|
|
56
50
|
console.log(t('xshell 正在退出').red);
|
|
@@ -88,7 +82,6 @@ export async function pollute_global() {
|
|
|
88
82
|
pollute_module_exports('./server.js'),
|
|
89
83
|
pollute_module_exports('./repl.js'),
|
|
90
84
|
]);
|
|
91
|
-
console.log(t('所有模块加载成功'));
|
|
92
85
|
}
|
|
93
86
|
export async function pollute_module_exports(fp_mod) {
|
|
94
87
|
Object.assign(globalThis, await import(fp_mod));
|
package/server.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="ua-parser-js" />
|
|
2
3
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
|
|
4
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
5
|
+
import { type Server as HttpServer, type IncomingHttpHeaders } from 'http';
|
|
6
|
+
import type { IncomingHttpHeaders as IncomingHttp2Headers } from 'http2';
|
|
4
7
|
import type { WebSocketServer } from 'ws';
|
|
5
8
|
import type { default as Koa, Context, Next } from 'koa';
|
|
6
|
-
import type { UserAgentContext } from 'koa-useragent';
|
|
7
9
|
declare module 'koa' {
|
|
8
10
|
interface Request {
|
|
9
11
|
/** 经过 decodeURIComponent 后,在路径重写之前的路径 */
|
|
@@ -12,9 +14,6 @@ declare module 'koa' {
|
|
|
12
14
|
}
|
|
13
15
|
interface Context {
|
|
14
16
|
compress: boolean;
|
|
15
|
-
userAgent: UserAgentContext['userAgent'] & {
|
|
16
|
-
isWechat: boolean;
|
|
17
|
-
};
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
19
|
import { type Remote, type Headers } from './net.js';
|
|
@@ -29,6 +28,19 @@ declare module 'http' {
|
|
|
29
28
|
export declare class Server {
|
|
30
29
|
/** proxy 时需要丢弃的 resposne headers */
|
|
31
30
|
static drop_response_headers: Set<string>;
|
|
31
|
+
UAParser: {
|
|
32
|
+
(uastring?: string, extensions?: Record<string, unknown>): import("ua-parser-js").IResult;
|
|
33
|
+
(extensions?: Record<string, unknown>): import("ua-parser-js").IResult;
|
|
34
|
+
new (uastring?: string, extensions?: Record<string, unknown>): import("ua-parser-js").UAParserInstance;
|
|
35
|
+
new (extensions?: Record<string, unknown>): import("ua-parser-js").UAParserInstance;
|
|
36
|
+
VERSION: string;
|
|
37
|
+
BROWSER: import("ua-parser-js").BROWSER;
|
|
38
|
+
CPU: import("ua-parser-js").CPU;
|
|
39
|
+
DEVICE: import("ua-parser-js").DEVICE;
|
|
40
|
+
ENGINE: import("ua-parser-js").ENGINE;
|
|
41
|
+
OS: import("ua-parser-js").OS;
|
|
42
|
+
UAParser: any;
|
|
43
|
+
};
|
|
32
44
|
js_exts: Set<string>;
|
|
33
45
|
app: Koa;
|
|
34
46
|
handler: ReturnType<Koa['callback']>;
|
|
@@ -45,15 +57,15 @@ export declare class Server {
|
|
|
45
57
|
start(): Promise<void>;
|
|
46
58
|
stop(): void;
|
|
47
59
|
entry(ctx: Context, next: Next): Promise<void>;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
process request.ip
|
|
51
|
-
*/
|
|
60
|
+
/** 解析 req.body to request.body
|
|
61
|
+
处理 request.ip */
|
|
52
62
|
parse(ctx: Context): Promise<void>;
|
|
53
63
|
_router(ctx: Context, next: Next): Promise<void>;
|
|
54
64
|
/** 被子类重写以自定义处理逻辑 */
|
|
55
65
|
router(ctx: Context): Promise<boolean>;
|
|
56
66
|
logger(ctx: Context): void;
|
|
67
|
+
process_ua(ctx: Context): string;
|
|
68
|
+
format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
|
|
57
69
|
proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>): Promise<void>;
|
|
58
70
|
static filter_response_headers(headers: Headers): {};
|
|
59
71
|
try_send(ctx: Context, fp: string, { root, log_404, }: {
|
package/server.js
CHANGED
|
@@ -3,7 +3,6 @@ import zlib from 'zlib';
|
|
|
3
3
|
import { createReadStream } from 'fs';
|
|
4
4
|
import { Readable } from 'stream';
|
|
5
5
|
import util from 'util';
|
|
6
|
-
import querystring from 'querystring';
|
|
7
6
|
// --- my libs
|
|
8
7
|
import { t } from './i18n/instance.js';
|
|
9
8
|
import { request as _request } from './net.js';
|
|
@@ -20,6 +19,7 @@ export class Server {
|
|
|
20
19
|
'transfer-encoding',
|
|
21
20
|
'x-powered-by'
|
|
22
21
|
]);
|
|
22
|
+
UAParser = null;
|
|
23
23
|
js_exts = new Set(['.js', '.mjs', '.cjs']);
|
|
24
24
|
app;
|
|
25
25
|
handler;
|
|
@@ -39,7 +39,8 @@ export class Server {
|
|
|
39
39
|
const { default: Koa } = await import('koa');
|
|
40
40
|
const { default: KoaCors } = await import('@koa/cors');
|
|
41
41
|
const { default: KoaCompress } = await import('koa-compress');
|
|
42
|
-
const {
|
|
42
|
+
const { UAParser } = await import('ua-parser-js');
|
|
43
|
+
this.UAParser = UAParser;
|
|
43
44
|
const { WebSocketServer } = await import('ws');
|
|
44
45
|
// --- init koa app
|
|
45
46
|
let app = new Koa();
|
|
@@ -60,7 +61,6 @@ export class Server {
|
|
|
60
61
|
threshold: 512
|
|
61
62
|
}));
|
|
62
63
|
app.use(KoaCors({ credentials: true }));
|
|
63
|
-
app.use(KoaUserAgent);
|
|
64
64
|
app.use(this._router.bind(this));
|
|
65
65
|
this.app = app;
|
|
66
66
|
this.handler = this.app.callback();
|
|
@@ -79,9 +79,23 @@ export class Server {
|
|
|
79
79
|
});
|
|
80
80
|
this.server_http.on('upgrade', (request, socket, head) => {
|
|
81
81
|
// url 只有路径部分
|
|
82
|
-
const { url, headers: { host = ''
|
|
82
|
+
const { url, headers, headers: { host = '' }, } = request;
|
|
83
83
|
const ip = request.socket.remoteAddress.replace(/^::ffff:/, '');
|
|
84
|
-
console.log(
|
|
84
|
+
console.log(
|
|
85
|
+
// 时间
|
|
86
|
+
`${new Date().to_time_str()} ` +
|
|
87
|
+
// ip(位置)
|
|
88
|
+
(ip || '').limit(60) + ' ' +
|
|
89
|
+
// ua
|
|
90
|
+
this.format_ua(headers).limit(56) + ' ' +
|
|
91
|
+
// https/2.0
|
|
92
|
+
`${this.colors ? 'websocket'.limit(10).magenta : 'websocket'.limit(10)} ` +
|
|
93
|
+
// method
|
|
94
|
+
''.limit(6) + ' ' +
|
|
95
|
+
// host
|
|
96
|
+
`${host.limit(24)} ` +
|
|
97
|
+
// path
|
|
98
|
+
(this.colors ? url.yellow : url));
|
|
85
99
|
switch (url) {
|
|
86
100
|
case '/':
|
|
87
101
|
this.server_ws.handleUpgrade(request, socket, head, ws => {
|
|
@@ -117,10 +131,8 @@ export class Server {
|
|
|
117
131
|
response.type = 'text/plain';
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
process request.ip
|
|
123
|
-
*/
|
|
134
|
+
/** 解析 req.body to request.body
|
|
135
|
+
处理 request.ip */
|
|
124
136
|
async parse(ctx) {
|
|
125
137
|
let { req, request } = ctx;
|
|
126
138
|
const buf = await stream_to_buffer(req);
|
|
@@ -131,14 +143,10 @@ export class Server {
|
|
|
131
143
|
// --- parse body
|
|
132
144
|
if (!req.body)
|
|
133
145
|
return;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else if (ctx.is('multipart/form-data'))
|
|
139
|
-
throw new Error('multipart/form-data is not supported');
|
|
140
|
-
else
|
|
141
|
-
request.body = req.body;
|
|
146
|
+
request.body = ctx.is('application/json') || ctx.is('text/plain') ?
|
|
147
|
+
JSON.parse(req.body.toString())
|
|
148
|
+
:
|
|
149
|
+
req.body;
|
|
142
150
|
}
|
|
143
151
|
async _router(ctx, next) {
|
|
144
152
|
let { request } = ctx;
|
|
@@ -167,49 +175,32 @@ export class Server {
|
|
|
167
175
|
const { request } = ctx;
|
|
168
176
|
const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, } = request;
|
|
169
177
|
let { method } = request;
|
|
170
|
-
const ua = ctx.userAgent;
|
|
171
178
|
let s = '';
|
|
172
|
-
//
|
|
173
|
-
s += `${new Date().to_time_str()}
|
|
174
|
-
//
|
|
175
|
-
s += (ip || '').
|
|
176
|
-
//
|
|
177
|
-
s += (()
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (ua.isDesktop)
|
|
182
|
-
t += 'desktop';
|
|
183
|
-
if (ua.isBot)
|
|
184
|
-
t += `${t ? ' ' : ''}${colors ? 'robot'.blue : 'robot'}`;
|
|
185
|
-
if (ua.platform !== 'unknown' && !ua.os.startsWith('Windows'))
|
|
186
|
-
t += '/' + ua.platform.toLowerCase().replace('apple mac', 'mac');
|
|
187
|
-
if (ua.os !== 'unknown' && ua.platform !== 'Android')
|
|
188
|
-
t += '/' + ua.os.toLowerCase();
|
|
189
|
-
if (ua.browser !== 'unknown')
|
|
190
|
-
t += '/' + ua.browser.toLowerCase();
|
|
191
|
-
if (ua.isWechat)
|
|
192
|
-
t += '/weixin';
|
|
193
|
-
if (ua.version !== 'unknown')
|
|
194
|
-
t += '/' + ua.version.split('.').slice(0, 2).join('.');
|
|
195
|
-
return t;
|
|
196
|
-
})().pad(40) + ' ';
|
|
197
|
-
// --- https/2.0
|
|
198
|
-
s += `${`${protocol.pad(5)}/${http_version}`.pad(10)} `;
|
|
199
|
-
// --- method
|
|
179
|
+
// 时间
|
|
180
|
+
s += `${new Date().to_time_str()} `;
|
|
181
|
+
// ip(位置)
|
|
182
|
+
s += (ip || '').limit(60) + ' ';
|
|
183
|
+
// ua
|
|
184
|
+
s += this.process_ua(ctx).limit(56) + ' ';
|
|
185
|
+
// https/2.0
|
|
186
|
+
s += `${`${protocol.limit(5)} ${http_version}`.limit(10)} `;
|
|
187
|
+
// method
|
|
200
188
|
method = method.toLowerCase();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
189
|
+
let t = method.limit(6) + ' ';
|
|
190
|
+
if (colors && method !== 'get')
|
|
191
|
+
method = t.yellow;
|
|
192
|
+
s += t;
|
|
193
|
+
// host
|
|
194
|
+
s += `${host.limit(24)} `;
|
|
195
|
+
// path
|
|
205
196
|
s += (() => {
|
|
206
197
|
if (path.toLowerCase() !== _path.toLowerCase())
|
|
207
|
-
return `${_path.blue}
|
|
198
|
+
return `${_path.blue} -> ${path}`;
|
|
208
199
|
if (!path.includes('.'))
|
|
209
200
|
return colors ? path.yellow : path;
|
|
210
201
|
return path;
|
|
211
202
|
})();
|
|
212
|
-
//
|
|
203
|
+
// query
|
|
213
204
|
if (Object.keys(query).length) {
|
|
214
205
|
let t = inspect(query, { compact: true })
|
|
215
206
|
.replace('[Object: null prototype] ', '');
|
|
@@ -218,12 +209,59 @@ export class Server {
|
|
|
218
209
|
s += (s + t).width > output_width ? '\n' : ' ';
|
|
219
210
|
s += t;
|
|
220
211
|
}
|
|
221
|
-
//
|
|
212
|
+
// body
|
|
222
213
|
if (body && Object.keys(body).length)
|
|
223
214
|
s += '\n' + inspect(body).replace('[Object: null prototype] ', '');
|
|
224
|
-
//
|
|
215
|
+
// 打印日志
|
|
225
216
|
console.log(s);
|
|
226
217
|
}
|
|
218
|
+
process_ua(ctx) {
|
|
219
|
+
const { headers, headers: { 'user-agent': ua_str } } = ctx.request;
|
|
220
|
+
if (!ua_str)
|
|
221
|
+
return '';
|
|
222
|
+
ctx.response.set('accept-ch', 'Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, RTT, Device-Memory');
|
|
223
|
+
return this.format_ua(headers);
|
|
224
|
+
}
|
|
225
|
+
format_ua(headers) {
|
|
226
|
+
const { rtt, 'device-memory': memory } = headers;
|
|
227
|
+
const { device, os, browser } = this.UAParser(headers).withClientHints();
|
|
228
|
+
const vendor = device.vendor?.toLowerCase();
|
|
229
|
+
const model = device.model?.toLowerCase();
|
|
230
|
+
const osname = os.name?.toLowerCase();
|
|
231
|
+
const browser_name = browser.name?.toLowerCase();
|
|
232
|
+
let s = '';
|
|
233
|
+
if (vendor && vendor !== 'apple')
|
|
234
|
+
s += vendor;
|
|
235
|
+
if (model && model !== 'k' && model !== 'macintosh') {
|
|
236
|
+
if (s)
|
|
237
|
+
s += ' ';
|
|
238
|
+
s += model.replace('22127rk46c', 'redmi k60 pro');
|
|
239
|
+
}
|
|
240
|
+
if (osname) {
|
|
241
|
+
if (s)
|
|
242
|
+
s += '/';
|
|
243
|
+
s += osname;
|
|
244
|
+
if (os.version)
|
|
245
|
+
s += ` ${os.version.split('.')[0].toLowerCase()}`;
|
|
246
|
+
}
|
|
247
|
+
if (browser_name) {
|
|
248
|
+
if (s)
|
|
249
|
+
s += '/';
|
|
250
|
+
s += browser_name.replace('mobile chrome', 'chrome');
|
|
251
|
+
if (browser.version) {
|
|
252
|
+
const [major, minor] = browser.version.toLowerCase().split('.');
|
|
253
|
+
s += ` ${major}`;
|
|
254
|
+
if (minor && minor !== '0')
|
|
255
|
+
s += `.${minor}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (memory) {
|
|
259
|
+
s += `/${memory}g`;
|
|
260
|
+
if (rtt && rtt !== '0')
|
|
261
|
+
s += ` ${rtt}ms`;
|
|
262
|
+
}
|
|
263
|
+
return s;
|
|
264
|
+
}
|
|
227
265
|
async proxy(ctx, url, headers_ = {}) {
|
|
228
266
|
const { request: { method, headers, query, body } } = ctx;
|
|
229
267
|
let { response } = ctx;
|
|
@@ -277,7 +315,7 @@ export class Server {
|
|
|
277
315
|
if (method !== 'HEAD' && method !== 'GET')
|
|
278
316
|
return false;
|
|
279
317
|
function _log_404() {
|
|
280
|
-
let s = `${' '.repeat(
|
|
318
|
+
let s = `${' '.repeat(11)} ${method.toLowerCase()} 404: ${path}`;
|
|
281
319
|
if (_path !== path)
|
|
282
320
|
s += ` ${_path.bracket()}`;
|
|
283
321
|
console.log(s.red);
|
package/utils.browser.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare const noop: () => void;
|
|
2
2
|
export declare function assert(assertion: any, message?: string): never | void;
|
|
3
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
4
|
+
@example seq(10, i => `item-${i}`) */
|
|
5
|
+
export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
|
|
3
6
|
export declare function delay(milliseconds: number): Promise<void>;
|
|
4
7
|
export declare function timeout(milliseconds: number): Promise<void>;
|
|
5
8
|
/** https://stackoverflow.com/questions/63297164/how-to-only-accept-arraybuffer-as-parameter */
|
package/utils.browser.js
CHANGED
|
@@ -6,6 +6,14 @@ export function assert(assertion, message) {
|
|
|
6
6
|
throw Object.assign(new Error(`断言失败: ${message ? `${message}: ` : ''}${assertion}`), { assertion });
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
10
|
+
@example seq(10, i => `item-${i}`) */
|
|
11
|
+
export function seq(n, generator) {
|
|
12
|
+
let a = new Array(n);
|
|
13
|
+
for (let i = 0; i < n; i++)
|
|
14
|
+
a[i] = generator ? generator(i) : i;
|
|
15
|
+
return a;
|
|
16
|
+
}
|
|
9
17
|
export async function delay(milliseconds) {
|
|
10
18
|
return new Promise(resolve => {
|
|
11
19
|
setTimeout(resolve, milliseconds);
|
package/utils.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export declare const noop: () => void;
|
|
|
10
10
|
export declare const output_width = 230;
|
|
11
11
|
export declare function set_inspect_options(colors?: boolean): void;
|
|
12
12
|
export declare function assert(assertion: any, message?: string): never | void;
|
|
13
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
14
|
+
@example seq(10, i => `item-${i}`) */
|
|
15
|
+
export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
|
|
13
16
|
export declare function dedent(templ: TemplateStringsArray | string, ...values: any[]): string;
|
|
14
17
|
/** 数组或 iterable 去重(可按 selector 去重)
|
|
15
18
|
- selector?: 可以是 key (string) 或 (obj: any) => any
|
package/utils.js
CHANGED
|
@@ -27,6 +27,14 @@ export function assert(assertion, message) {
|
|
|
27
27
|
throw Object.assign(new Error(`断言失败: ${message ? `${message}: ` : ''}${inspect(assertion, { colors: false, compact: true })}`), { assertion });
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
31
|
+
@example seq(10, i => `item-${i}`) */
|
|
32
|
+
export function seq(n, generator) {
|
|
33
|
+
let a = new Array(n);
|
|
34
|
+
for (let i = 0; i < n; i++)
|
|
35
|
+
a[i] = generator ? generator(i) : i;
|
|
36
|
+
return a;
|
|
37
|
+
}
|
|
30
38
|
export function dedent(templ, ...values) {
|
|
31
39
|
let strings = Array.from(typeof templ === 'string' ? [templ] : templ.raw);
|
|
32
40
|
// 1. remove trailing whitespace
|
|
@@ -397,8 +405,8 @@ export async function stream_to_buffer(stream) {
|
|
|
397
405
|
export async function* stream_to_lines(stream) {
|
|
398
406
|
let buf = '';
|
|
399
407
|
for await (const chunk of stream) {
|
|
400
|
-
let
|
|
401
|
-
for (; (i = chunk.indexOf('\n', j)) >= 0;) {
|
|
408
|
+
let j = 0;
|
|
409
|
+
for (let i = 0; (i = chunk.indexOf('\n', j)) >= 0;) {
|
|
402
410
|
let line = chunk.slice(j, i);
|
|
403
411
|
if (buf) {
|
|
404
412
|
line = buf + line;
|