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 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: string | number, { mode, print }?: {
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, '', fp_dst);
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, '', dst);
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, '', 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')} ${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')} ${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/114.0.0.0 Safari/537.36',
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.48",
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.0",
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.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.1",
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.4.9",
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.3.0",
107
- "@typescript-eslint/parser": "^6.3.0",
108
- "eslint": "^8.46.0",
109
- "eslint-plugin-react": "^7.33.1",
110
- "eslint-plugin-xlint": "^1.0.6"
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
- const t = s.slice(0, i_fitted_next) + ' '.repeat(width - 2 - fitted_width) + '…';
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, delta2str } from './utils.js';
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(('-'.repeat(30) + '\n' +
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
- import { type Server as HttpServer } from 'http';
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
- parse req.body to request.body
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 { userAgent: KoaUserAgent } = await import('koa-useragent');
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 = '', 'user-agent': ua }, } = request;
82
+ const { url, headers, headers: { host = '' }, } = request;
83
83
  const ip = request.socket.remoteAddress.replace(/^::ffff:/, '');
84
- console.log(`${new Date().to_time_str()} ${(ip || '').pad(40)} ${(ua || '').limit(40)} ${'websocket'.pad(10).magenta} ${'connect'.pad(10).magenta}${host.pad(20)} ${url.pad(60).yellow}`);
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
- parse req.body to request.body
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
- if (ctx.is('application/json') || ctx.is('text/plain'))
135
- request.body = JSON.parse(req.body.toString());
136
- else if (ctx.is('application/x-www-form-urlencoded'))
137
- request.body = querystring.parse(req.body.toString());
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
- // --- time
173
- s += `${new Date().to_time_str()} `;
174
- // --- ip
175
- s += (ip || '').pad(40) + ' ';
176
- // --- ua
177
- s += (() => {
178
- let t = '';
179
- if (ua.isMobile)
180
- t += 'mobile';
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
- s += (method === 'get' || !colors) ? method.pad(10) : method.pad(10).yellow;
202
- // --- host
203
- s += `${host.pad(20)} `;
204
- // --- path
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} ${path}`;
198
+ return `${_path.blue} -> ${path}`;
208
199
  if (!path.includes('.'))
209
200
  return colors ? path.yellow : path;
210
201
  return path;
211
202
  })();
212
- // --- query
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
- // --- body
212
+ // body
222
213
  if (body && Object.keys(body).length)
223
214
  s += '\n' + inspect(body).replace('[Object: null prototype] ', '');
224
- // --- print log
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(13)} ${method.toLowerCase()} 404: ${path}`;
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);
@@ -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 i = 0, j = 0;
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;