xshell 1.0.78 → 1.0.79

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/Terminal.js CHANGED
@@ -4,6 +4,9 @@ import { useEffect, useRef } from 'react';
4
4
  import { Terminal as XTermTerminal } from 'xterm';
5
5
  import { FitAddon } from 'xterm-addon-fit';
6
6
  import { WebglAddon } from 'xterm-addon-webgl';
7
+ import { WebLinksAddon } from 'xterm-addon-web-links';
8
+ // 没有 ui
9
+ // import { SearchAddon } from 'xterm-addon-search'
7
10
  import { Model } from 'react-object-model';
8
11
  import { assert, genid } from './utils.browser.js';
9
12
  export function Terminal({ font }) {
@@ -25,7 +28,11 @@ export function Terminal({ font }) {
25
28
  }
26
29
  });
27
30
  const fit_addon = new FitAddon();
31
+ const link_addon = new WebLinksAddon();
32
+ // const search_addon = new SearchAddon()
28
33
  term.loadAddon(fit_addon);
34
+ term.loadAddon(link_addon);
35
+ // term.loadAddon(search_addon)
29
36
  term.open(rterminal.current);
30
37
  term.loadAddon(new WebglAddon());
31
38
  fit_addon.fit();
package/file.d.ts CHANGED
@@ -105,7 +105,7 @@ export declare function ffstat(handle: fsp.FileHandle): Promise<fs.BigIntStats>;
105
105
 
106
106
  返回是否实际进行了删除操作
107
107
  Returns whether the delete operation actually took place */
108
- export declare function fdelete(fp: string, { print, red }?: {
108
+ export declare function fdelete(fp: string, { print }?: {
109
109
  print?: boolean;
110
110
  red?: boolean;
111
111
  }): Promise<boolean>;
package/file.js CHANGED
@@ -194,16 +194,14 @@ export async function ffstat(handle) {
194
194
 
195
195
  返回是否实际进行了删除操作
196
196
  Returns whether the delete operation actually took place */
197
- export async function fdelete(fp, { print = true, red = true } = {}) {
197
+ export async function fdelete(fp, { print = true } = {}) {
198
198
  assert(fp.length >= 6, `fp: ${fp} ${t('不能太短,防止误删文件')}`);
199
199
  assert(path.isAbsolute(fp), t('fp 必须是绝对路径'));
200
200
  const { isdir } = fp;
201
201
  try {
202
202
  await fsp.rm(fp, { recursive: true });
203
- if (print) {
204
- const message = `${isdir ? t('删除了文件夹') : t('删除了文件')} ${fp}`;
205
- console.log(red ? message.red : message);
206
- }
203
+ if (print)
204
+ console.log(`${isdir ? t('删除了文件夹') : t('删除了文件')} ${fp}`);
207
205
  return true;
208
206
  }
209
207
  catch (error) {
@@ -381,7 +379,7 @@ export async function fzip(data, fp_zip, { dirname, print = { files: true, info:
381
379
  else
382
380
  entries = data;
383
381
  if (print.info)
384
- console.log(`开始压缩${fpd_src ? ` ${fpd_src}` : '文件索引'} -> ${fp_zip ? `${fp_zip}/${dirname}` : '内存'}`);
382
+ console.log(`开始压缩${fpd_src ? ` ${fpd_src}` : '文件索引'} -> ${fp_zip ? `${fp_zip}/${dirname || '{entries}'}` : '内存'}`);
385
383
  const { default: archiver } = await import('archiver');
386
384
  let archive = archiver('zip', { zlib: { chunkSize: 16 * 2 ** 20 /* 16 MB */ } });
387
385
  let ostream = fp_zip ?
package/git.d.ts CHANGED
@@ -33,7 +33,8 @@ export declare class Git {
33
33
  checkout(branch?: 'main'): Promise<void>;
34
34
  checkout(branch?: string): Promise<void>;
35
35
  _checkout(branch?: string): Promise<void>;
36
- merge(branch: string, fast_forward?: boolean): Promise<void>;
36
+ /** 返回是否进行了修改 */
37
+ merge(branch: string, fast_forward?: boolean): Promise<boolean>;
37
38
  /** 记住当前分支名
38
39
  checkout 到 <目标分支>
39
40
  merge 当前分支 */
package/git.js CHANGED
@@ -121,6 +121,7 @@ export class Git {
121
121
  throw error;
122
122
  }
123
123
  }
124
+ /** 返回是否进行了修改 */
124
125
  async merge(branch, fast_forward = true) {
125
126
  console.log(`合并 ${branch} 分支`);
126
127
  try {
@@ -129,12 +130,14 @@ export class Git {
129
130
  branch,
130
131
  ...fast_forward ? [] : ['--no-ff']
131
132
  ], { print: false });
132
- if (stdout === 'Already up to date.\n')
133
+ const unchanged = stdout === 'Already up to date.\n';
134
+ if (unchanged)
133
135
  console.log('当前分支无需更新');
134
136
  else
135
137
  console.log(stdout.trimEnd());
136
138
  if (stderr)
137
139
  console.log(stderr);
140
+ return !unchanged;
138
141
  }
139
142
  catch (error) {
140
143
  if (error.result) {
package/i18n/dict.json CHANGED
@@ -373,5 +373,8 @@
373
373
  },
374
374
  "已不存在文件夹": {
375
375
  "en": "folder no longer exists:"
376
+ },
377
+ "不支持 content-encoding: {{encoding}} 的 http 请求": {
378
+ "en": "http requests with content-encoding: {{encoding}} are not supported"
376
379
  }
377
380
  }
package/net.browser.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import './prototype.browser.js';
2
2
  import { Lock } from './utils.browser.js';
3
+ export interface FullResponse<TBody extends ArrayBuffer | string> {
4
+ status: number;
5
+ headers: Headers;
6
+ body: TBody;
7
+ }
3
8
  export interface BasicAuth {
4
9
  type: 'basic';
5
10
  username: string;
@@ -25,19 +30,13 @@ export interface RequestOptions {
25
30
  export interface RequestRawOptions extends RequestOptions {
26
31
  raw: true;
27
32
  }
33
+ export interface RequestFullOptions extends RequestOptions {
34
+ full: true;
35
+ }
28
36
  export interface RequestError extends Error {
29
37
  url: URL;
30
- options: RequestInit;
31
- response?: {
32
- /** 状态码 */
33
- status: number;
34
- url: string;
35
- headers: Headers;
36
- ok: boolean;
37
- type: ResponseType;
38
- text: string;
39
- redirected: boolean;
40
- };
38
+ options: RequestOptions | RequestFullOptions | RequestRawOptions;
39
+ response?: FullResponse<string>;
41
40
  }
42
41
  /**
43
42
  - url: 可以只有 pathname 部分
@@ -55,6 +54,10 @@ export interface RequestError extends Error {
55
54
  - raw?: `false` 传入后返回整个 response */
56
55
  export declare function request(url: string | URL): Promise<string>;
57
56
  export declare function request(url: string | URL, options: RequestRawOptions): Promise<Response>;
57
+ export declare function request(url: string | URL, options: RequestFullOptions & {
58
+ encoding: 'binary';
59
+ }): Promise<FullResponse<ArrayBuffer>>;
60
+ export declare function request(url: string | URL, options: RequestFullOptions): Promise<FullResponse<string>>;
58
61
  export declare function request(url: string | URL, options: RequestOptions & {
59
62
  encoding: 'binary';
60
63
  }): Promise<ArrayBuffer>;
package/net.browser.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { t } from './i18n/instance.js';
2
2
  import './prototype.browser.js'; // to_time_str()
3
3
  import { assert, concat, genid, delay, Lock, encode, decode } from './utils.browser.js';
4
- // ------------------------------------ fetch, request
5
4
  const drop_request_headers = new Set([
6
5
  // : 开头的 key
7
6
  // sec-*
@@ -32,7 +31,9 @@ async function fetch_retry(url, options, timeout, retries = 0, count = 0) {
32
31
  }
33
32
  }
34
33
  }
35
- export async function request(url, { method, queries, headers: _headers, body, type = 'application/json', encoding, retries, timeout = 5 * 1000, auth, raw = false, cors, } = {}) {
34
+ export async function request(url, options = {}) {
35
+ const { queries, headers: _headers, body, type = 'application/json', encoding, timeout = 5 * 1000, auth, cors, raw = false, full = false, } = options;
36
+ let { method, retries, } = options;
36
37
  url = new URL(url, location.href);
37
38
  if (queries)
38
39
  for (const key in queries) {
@@ -61,10 +62,10 @@ export async function request(url, { method, queries, headers: _headers, body, t
61
62
  else
62
63
  for (const key in _headers)
63
64
  if (!key.startsWith(':') && !key.startsWith('sec-') && !drop_request_headers.has(key)) { // 可能在 http/2 的 response 中会有这样开头的保留 headers, 在透传时忽略比较好
64
- assert(key === key.toLowerCase(), t('传入 request 的 headers 参数中 key 应该都是小写的,实际为 {{key}}', { key }));
65
+ assert(key === key.toLowerCase(), `传入 request 的 headers 参数中 key 应该都是小写的,实际为 ${key}`);
65
66
  headers.set(key, _headers[key]);
66
67
  }
67
- let options = {
68
+ let fetch_options = {
68
69
  ...method ? { method } : {},
69
70
  keepalive: true,
70
71
  redirect: 'follow',
@@ -102,33 +103,39 @@ export async function request(url, { method, queries, headers: _headers, body, t
102
103
  };
103
104
  let response;
104
105
  try {
105
- response = await fetch_retry(url, options, timeout, retries);
106
+ response = await fetch_retry(url, fetch_options, timeout, retries);
106
107
  if (!response.ok)
107
108
  throw Object.assign(new Error(t('状态码 {{status}}, 非 2xx', { status: response.status })), { name: 'StatusCodeError' });
108
109
  }
109
110
  catch (error) {
111
+ ;
110
112
  error.url = url;
111
113
  error.options = options;
112
114
  if (response)
113
115
  error.response = {
114
116
  status: response.status,
115
- url: response.url,
116
117
  headers: response.headers,
117
- ok: response.ok,
118
- type: response.type,
119
- text: await response.text(),
120
- redirected: response.redirected
118
+ body: await response.text(),
121
119
  };
122
120
  throw error;
123
121
  }
124
122
  if (raw)
125
123
  return response;
126
- if (!response.body)
127
- return response.body;
128
- if (encoding === 'binary')
129
- return response.arrayBuffer();
130
- else
131
- return response.text();
124
+ const body_ = await (async () => {
125
+ if (!response.body)
126
+ return encoding === 'binary' ? new ArrayBuffer(0) : '';
127
+ if (encoding === 'binary')
128
+ return response.arrayBuffer();
129
+ else
130
+ return response.text();
131
+ })();
132
+ return full ? {
133
+ status: response.status,
134
+ headers: response.headers,
135
+ body: body_
136
+ }
137
+ :
138
+ body_;
132
139
  }
133
140
  /** 发起 http 请求并将响应体作为 json 解析 */
134
141
  export async function request_json(url, options) {
package/net.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- import type { FormData, Headers, Response, RequestInit } from 'undici';
2
+ /// <reference types="node" resolution-mode="require"/>
3
+ import type { Readable } from 'stream';
4
+ import type { FormData } from 'undici';
3
5
  import type { WebSocket, CloseEvent, ErrorEvent } from 'ws';
4
6
  import type { Cookie, CookieJar, MemoryCookieStore } from 'tough-cookie';
5
7
  declare module 'tough-cookie' {
@@ -10,7 +12,7 @@ declare module 'tough-cookie' {
10
12
  import './prototype.js';
11
13
  import type { Encoding } from './file.js';
12
14
  import { inspect, Lock } from './utils.js';
13
- export type { WebSocket, Headers };
15
+ export type { WebSocket };
14
16
  export declare const WebSocketConnecting = 0;
15
17
  export declare const WebSocketOpen = 1;
16
18
  export declare const WebSocketClosing = 2;
@@ -23,9 +25,18 @@ export declare const cookies: {
23
25
  store: MemoryCookieStore;
24
26
  jar: CookieJar;
25
27
  init(): Promise<void>;
26
- get(domain_or_url: string, str?: boolean): Cookie[] | Promise<Cookie[] | string>;
27
28
  };
28
29
  export type { Cookie };
30
+ export interface RawResponse {
31
+ status: number;
32
+ headers: Record<string, string>;
33
+ body: Readable;
34
+ }
35
+ export interface FullResponse<TBody extends Buffer | string> {
36
+ status: number;
37
+ headers: Record<string, string>;
38
+ body: TBody;
39
+ }
29
40
  export interface BasicAuth {
30
41
  type: 'basic';
31
42
  username: string;
@@ -38,8 +49,8 @@ export interface BearerAuth {
38
49
  export interface RequestOptions {
39
50
  method?: 'GET' | 'POST' | 'PUT' | 'HEAD' | 'DELETE' | 'PATCH';
40
51
  queries?: Record<string, any>;
41
- headers?: Record<string, string> | Headers;
42
- body?: string | Record<string, any> | NodeJS.ArrayBufferView | ArrayBuffer | AsyncIterable<Uint8Array> | Iterable<Uint8Array> | URLSearchParams | FormData;
52
+ headers?: Record<string, string>;
53
+ body?: string | Record<string, any> | Uint8Array | URLSearchParams | FormData;
43
54
  type?: 'application/json' | 'application/x-www-form-urlencoded' | 'multipart/form-data';
44
55
  proxy?: boolean | MyProxy | string;
45
56
  encoding?: Encoding | 'binary';
@@ -49,22 +60,16 @@ export interface RequestOptions {
49
60
  cookies?: Record<string, string>;
50
61
  redirect?: RequestRedirect;
51
62
  }
63
+ export interface RequestFullOptions extends RequestOptions {
64
+ full: true;
65
+ }
52
66
  export interface RequestRawOptions extends RequestOptions {
53
67
  raw: true;
54
68
  }
55
69
  export interface RequestError extends Error {
56
70
  url: URL;
57
- options: RequestInit;
58
- response?: {
59
- /** 状态码 */
60
- status: number;
61
- url: string;
62
- headers: Headers;
63
- ok: boolean;
64
- type: ResponseType;
65
- text: string;
66
- redirected: boolean;
67
- };
71
+ options: RequestOptions | RequestFullOptions | RequestRawOptions;
72
+ response?: FullResponse<string>;
68
73
  [inspect.custom]: Function;
69
74
  }
70
75
  /**
@@ -72,25 +77,34 @@ export interface RequestError extends Error {
72
77
  - options?:
73
78
  - method?: `有 body 时为 POST, 否则为 GET` 'GET' | 'POST' | ···
74
79
  - queries?: 添加到 url 上的参数,是 Record<string, any>,true/false 会被转换为 0/1
75
- - headers?: http 请求头 (Record<string, string> 或者 Headers 类型),其中 key 必须是小写的
76
- - body?: http 请求体,可以是 string, Record<string, any> (会自动 JSON.stringify), ArrayBuffer(View), (Async)Iterable<Uint8Array>
77
- URLSearchParams (type 为 x-www-form-urlencoded), FormData (type 为 form-data)
80
+ - headers?: http 请求头,是 Record<string, string>,其中 key 必须是小写的
81
+ - body?: http 请求体,可以是:
82
+ - string
83
+ - Uint8Array
84
+ - Record<string, any>: 会自动 JSON.stringify
85
+ - URLSearchParams: 仅限 type 为 x-www-form-urlencoded
86
+ - FormData: 仅限 type 为 form-data
78
87
  - type?: `'application/json'` 有 body 时设置 http 请求头中的 content-type 头
79
88
  - proxy?: `false` 通过代理发送请求
80
89
  - 为 true 时使用 MyProxy.socks5
81
90
  - 为非空 string 作为代理地址
82
91
  - 为 falsy 值时设为 false
83
- - encoding?: `根据网页 content-type: charset=gb18030 提取 || 'utf-8'` 传入 'binary' 时返回 ArrayBuffer
92
+ - encoding?: `根据网页 content-type: charset=gb18030 提取 || 'utf-8'` 传入 'binary' 时返回 Buffer
84
93
  - retries?: `false` 可以传入 true (默认 2 次) 或 重试次数
85
94
  - timeout?: `5 * 1000`
86
95
  - auth?: BasicAuth | BearerAuth
87
96
  - cookies?: 需要额外添加到请求的 cookies, 默认情况下携带了 MemoryCookieStore 中保存的之前 http 响应中的 cookies
88
- - raw?: `false` 传入后返回整个 response */
97
+ - raw?: `false` 传入后返回整个 response (RawResponse),body 为 Readable
98
+ - full?: `false` 传入后返回整个 response (FullResponse),body 根据 encoding 对应为 string 或 Buffer */
89
99
  export declare function request(url: string | URL): Promise<string>;
90
- export declare function request(url: string | URL, options: RequestRawOptions): Promise<Response>;
100
+ export declare function request(url: string | URL, options: RequestRawOptions): Promise<RawResponse>;
101
+ export declare function request(url: string | URL, options: RequestFullOptions & {
102
+ encoding: 'binary';
103
+ }): Promise<FullResponse<Buffer>>;
104
+ export declare function request(url: string | URL, options: RequestFullOptions): Promise<FullResponse<string>>;
91
105
  export declare function request(url: string | URL, options: RequestOptions & {
92
106
  encoding: 'binary';
93
- }): Promise<ArrayBuffer>;
107
+ }): Promise<Buffer>;
94
108
  export declare function request(url: string | URL, options: RequestOptions): Promise<string>;
95
109
  /** 发起 http 请求并将响应体作为 json 解析 */
96
110
  export declare function request_json<T = any>(url: string | URL, options?: RequestOptions): Promise<T>;
package/net.js CHANGED
@@ -1,6 +1,8 @@
1
+ import zlib from 'zlib';
2
+ import { buffer as stream_to_buffer, text as stream_to_text } from 'stream/consumers';
1
3
  import { t } from './i18n/instance.js';
2
4
  import './prototype.js';
3
- import { inspect, concat, assert, genid, delay, Lock, encode, decode } from './utils.js';
5
+ import { inspect, concat, assert, genid, delay, Lock, encode, decode, pipe_with_error, map_values, unique } from './utils.js';
4
6
  export const WebSocketConnecting = 0;
5
7
  export const WebSocketOpen = 1;
6
8
  export const WebSocketClosing = 2;
@@ -20,20 +22,6 @@ export const cookies = {
20
22
  const { MemoryCookieStore, CookieJar } = await import('tough-cookie');
21
23
  this.jar = new CookieJar(this.store = new MemoryCookieStore());
22
24
  },
23
- get(domain_or_url, str = false) {
24
- if (domain_or_url.startsWith('http'))
25
- if (str)
26
- return this.jar.getCookieString(domain_or_url);
27
- else
28
- return this.jar.getCookies(domain_or_url);
29
- let cookies;
30
- this.store.findCookies(domain_or_url, null, true, (error, _cookies) => {
31
- if (error)
32
- throw error;
33
- cookies = _cookies;
34
- });
35
- return cookies;
36
- },
37
25
  };
38
26
  /** 对于 request() 函数来说无意义的 headers,会自动过滤掉 */
39
27
  const drop_request_headers = new Set([
@@ -48,15 +36,11 @@ const drop_request_headers = new Set([
48
36
  'upgrade',
49
37
  ]);
50
38
  let proxy_agents = {};
51
- let fetch_cookie;
52
- async function fetch_retry(url, options, timeout, retries = 0, count = 0) {
53
- const { fetch } = await import('undici');
54
- const { default: make_fetch_cookie } = await import('fetch-cookie');
55
- await cookies.init();
56
- fetch_cookie ||= make_fetch_cookie(fetch, cookies.jar, false);
39
+ async function request_retry(url, options, timeout, retries = 0, count = 0) {
40
+ const { request: undici_request } = await import('undici');
57
41
  try {
58
42
  options.signal = AbortSignal.timeout(timeout);
59
- return await fetch_cookie(url, options);
43
+ return await undici_request(url, options);
60
44
  }
61
45
  catch (error) {
62
46
  if (count >= retries ||
@@ -66,12 +50,16 @@ async function fetch_retry(url, options, timeout, retries = 0, count = 0) {
66
50
  const duration = 2 ** count;
67
51
  console.log(`${t('等待 {{duration}} 秒后重试 fetch ({{_count}}) …', { duration, _count: count }).yellow} ${url.toString().blue.underline}`);
68
52
  await delay(1000 * duration);
69
- return fetch_retry(url, options, timeout, retries, count + 1);
53
+ return request_retry(url, options, timeout, retries, count + 1);
70
54
  }
71
55
  }
72
56
  }
73
- export async function request(url, { method, queries, headers: _headers, body, type = 'application/json', proxy, encoding, retries, timeout = 5 * 1000, auth, cookies: _cookies, raw = false, redirect = 'follow', } = {}) {
74
- const { Headers, ProxyAgent, FormData } = await import('undici');
57
+ export async function request(url, options = {}) {
58
+ const { ProxyAgent, FormData } = await import('undici');
59
+ const { Cookie } = await import('tough-cookie');
60
+ await cookies.init();
61
+ const { queries, headers: _headers, body, type = 'application/json', timeout = 5 * 1000, auth, cookies: _cookies, raw = false, full = false, redirect = 'follow', } = options;
62
+ let { method, retries, encoding, proxy, } = options;
75
63
  url = new URL(url);
76
64
  if (queries)
77
65
  for (const key in queries) {
@@ -80,38 +68,46 @@ export async function request(url, { method, queries, headers: _headers, body, t
80
68
  value = value ? '1' : '0';
81
69
  url.searchParams.append(key, value);
82
70
  }
71
+ const urlstr = url.toString();
83
72
  if (body !== undefined && !method)
84
73
  method = 'POST';
85
74
  if (retries === true)
86
75
  retries = 2;
87
76
  // --- headers, http/2 开始都用小写的 headers
88
- let headers = new Headers({
77
+ let headers = {
89
78
  '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/119.0.0.0 Safari/537.36',
79
+ 'accept-encoding': 'gzip, deflate, br',
80
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
91
81
  'sec-ch-ua-platform': '"Windows"',
92
82
  'sec-ch-ua-platform-version': '"15.0.0"',
93
- });
83
+ };
94
84
  if (body !== undefined)
95
- headers.set('content-type', type);
85
+ headers['content-type'] = type;
96
86
  if (auth)
97
- headers.set('authorization', auth.type === 'basic' ? `Basic ${`${auth.username}:${auth.password}`.to_base64()}` : `Bearer ${auth.token}`);
98
- if (_cookies)
99
- headers.set('cookie', Object.entries(_cookies)
100
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
101
- .join('; '));
102
- if (_headers)
103
- if (_headers instanceof Headers)
104
- for (const [key, value] of _headers) {
105
- if (!key.startsWith(':') && !key.startsWith('sec-') && !drop_request_headers.has(key))
106
- headers.set(key, value);
87
+ headers.authorization = auth.type === 'basic' ?
88
+ `Basic ${`${auth.username}:${auth.password}`.to_base64()}`
89
+ :
90
+ `Bearer ${auth.token}`;
91
+ const request_cookies = unique([
92
+ ...cookies.jar.getCookiesSync(urlstr),
93
+ ..._cookies ?
94
+ Object.entries(_cookies).map(([key, value]) => new Cookie({ key, value }))
95
+ :
96
+ []
97
+ ], 'key');
98
+ if (request_cookies.length)
99
+ headers.cookie = request_cookies.map(cookie => cookie.cookieString())
100
+ .join('; ');
101
+ if (_headers) {
102
+ assert(Object.getPrototypeOf(_headers)?.constructor.name !== 'Headers');
103
+ for (const key in _headers)
104
+ // 可能在 http/2 的 response 中会有这样开头的保留 headers, 在透传时忽略比较好
105
+ if (!key.startsWith(':') && !key.startsWith('sec-') && !drop_request_headers.has(key)) {
106
+ assert(key === key.toLowerCase(), t('传入 request 的 headers 参数中 key 应该都是小写的,实际为 {{key}}', { key }));
107
+ headers[key] = _headers[key];
107
108
  }
108
- else
109
- for (const key in _headers)
110
- if (!key.startsWith(':') && !key.startsWith('sec-') && !drop_request_headers.has(key)) { // 可能在 http/2 的 response 中会有这样开头的保留 headers, 在透传时忽略比较好
111
- assert(key === key.toLowerCase(), t('传入 request 的 headers 参数中 key 应该都是小写的,实际为 {{key}}', { key }));
112
- headers.set(key, _headers[key]);
113
- }
114
- let options = {
109
+ }
110
+ let undici_options = {
115
111
  ...method ? { method } : {},
116
112
  dispatcher: (() => {
117
113
  if (proxy) {
@@ -120,27 +116,23 @@ export async function request(url, { method, queries, headers: _headers, body, t
120
116
  return proxy_agents[proxy] ??= new ProxyAgent({ uri: proxy });
121
117
  }
122
118
  })(),
123
- keepalive: true,
124
- redirect,
125
- maxRedirect: 6,
126
- credentials: 'include',
119
+ // todo: 强制手动处理重定向,来正确处理 cookie ?
120
+ maxRedirections: redirect === 'follow' ? 5 : 0,
127
121
  headers,
128
122
  // --- body
129
123
  body: (() => {
130
124
  if (body === undefined)
131
125
  return;
132
126
  switch (type) {
133
- case 'application/json':
134
- return typeof body === 'string' ||
135
- ArrayBuffer.isView(body) ||
136
- body instanceof ArrayBuffer ||
137
- // 测试 Iterable<Uint8Array>, AsyncIterable<Uint8Array>
138
- (!Array.isArray(body) && (Symbol.iterator in Object(body) || Symbol.asyncIterator in Object(body))) ?
139
- body
140
- :
141
- JSON.stringify(body);
127
+ case 'application/json': // 可能的类型 string | Record<string, any> | Uint8Array
128
+ if (typeof body === 'string')
129
+ return body;
130
+ if (body instanceof Uint8Array)
131
+ return body;
132
+ assert(!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)));
133
+ return JSON.stringify(body);
142
134
  case 'application/x-www-form-urlencoded':
143
- return body instanceof URLSearchParams ? body : new URLSearchParams(body);
135
+ return (body instanceof URLSearchParams ? body : new URLSearchParams(body)).toString();
144
136
  case 'multipart/form-data':
145
137
  if (body instanceof FormData)
146
138
  return body;
@@ -157,29 +149,64 @@ export async function request(url, { method, queries, headers: _headers, body, t
157
149
  };
158
150
  let response;
159
151
  try {
160
- response = await fetch_retry(url, options, timeout, retries);
161
- if (!(response.ok || (redirect === 'manual' && 300 <= response.status && response.status < 400)))
152
+ const { statusCode: status, headers: _headers, body: _body } = await request_retry(url, undici_options, timeout, retries);
153
+ // 处理 cookie,自动保存到 cookie jar
154
+ let _cookies = _headers['set-cookie'] || [];
155
+ if (typeof _cookies === 'string')
156
+ _cookies = [_cookies];
157
+ _cookies.map(cookie => cookies.jar.setCookieSync(cookie, urlstr, { ignoreError: true }));
158
+ const headers = map_values(_headers, value => Array.isArray(value) ? value.join(', ') : (value || ''));
159
+ // UndiciResponse.body 没有自动根据 content-encoding 来解码,这里手动处理并替换 body 为解码后的 stream
160
+ let body = _body;
161
+ const content_encoding = headers['content-encoding']?.trim().toLowerCase();
162
+ if (content_encoding) {
163
+ assert(!content_encoding.includes(','));
164
+ switch (content_encoding) {
165
+ case 'gzip':
166
+ case 'x-gzip':
167
+ body = pipe_with_error(_body, zlib.createGunzip({
168
+ // Be less strict when decoding compressed responses, since sometimes
169
+ // servers send slightly invalid responses that are still accepted
170
+ // by common browsers.
171
+ // Always using Z_SYNC_FLUSH is what cURL does.
172
+ flush: zlib.constants.Z_SYNC_FLUSH,
173
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
174
+ }));
175
+ break;
176
+ case 'deflate':
177
+ body = pipe_with_error(_body, zlib.createInflate());
178
+ break;
179
+ case 'br':
180
+ body = pipe_with_error(_body, zlib.createBrotliDecompress());
181
+ break;
182
+ default:
183
+ throw new Error(t('不支持 content-encoding: {{encoding}} 的 http 请求', { encoding: content_encoding.quote() }));
184
+ }
185
+ }
186
+ response = {
187
+ status,
188
+ headers,
189
+ body
190
+ };
191
+ if (!((200 <= status && status <= 299) || (redirect === 'manual' && 300 <= status && status < 400)))
162
192
  throw Object.assign(new Error(t('状态码 {{status}} 非 2xx', { status: response.status })), { name: 'StatusCodeError' });
163
193
  }
164
194
  catch (error) {
195
+ ;
165
196
  error.url = url;
166
197
  error.options = options;
167
198
  if (response)
168
199
  error.response = {
169
200
  status: response.status,
170
- url: response.url,
171
201
  headers: response.headers,
172
- ok: response.ok,
173
- type: response.type,
174
- text: await response.text(),
175
- redirected: response.redirected
202
+ body: await stream_to_text(response.body),
176
203
  };
177
204
  error[inspect.custom] = (depth, options, inspect) => {
178
205
  const { colors } = options;
179
206
  let _method = method || 'GET';
180
207
  if (colors)
181
208
  _method = _method.red;
182
- let _url = url.toString();
209
+ let _url = urlstr;
183
210
  if (colors)
184
211
  _url = _url.blue.underline;
185
212
  let s = '\n' +
@@ -218,14 +245,16 @@ export async function request(url, { method, queries, headers: _headers, body, t
218
245
  if (colors)
219
246
  _headers = _headers.yellow;
220
247
  s += _headers + '\n';
221
- for (const [key, value] of response.headers)
248
+ for (const key in response.headers) {
249
+ const value = response.headers[key];
222
250
  s += `${key}: ${value}\n`;
223
- if (error.response.text) {
251
+ }
252
+ if (error.response.body) {
224
253
  let _body = t('响应体:');
225
254
  if (colors)
226
255
  _body = _body.yellow;
227
256
  s += _body + '\n' +
228
- error.response.text + '\n';
257
+ error.response.body + '\n';
229
258
  }
230
259
  }
231
260
  let _stack = t('调用栈:');
@@ -242,27 +271,35 @@ export async function request(url, { method, queries, headers: _headers, body, t
242
271
  }
243
272
  if (raw)
244
273
  return response;
245
- if (!response.body)
246
- return response.body;
247
- // --- decode body
248
- if (encoding === 'binary')
249
- return response.arrayBuffer();
250
- encoding ||= /charset=(.*)/.exec(response.headers.get('content-type'))?.[1] || 'utf-8';
251
- if (/utf-?8/i.test(encoding))
252
- return response.text();
253
- return new TextDecoder(encoding)
254
- .decode(await response.arrayBuffer());
274
+ const body_ = await (async () => {
275
+ if (!response.body)
276
+ return encoding === 'binary' ? Buffer.from([]) : '';
277
+ if (encoding === 'binary')
278
+ return stream_to_buffer(response.body);
279
+ encoding ||= /charset=(.*)/.exec(response.headers['content-type'])?.[1] || 'utf-8';
280
+ if (/utf-?8/i.test(encoding))
281
+ return stream_to_text(response.body);
282
+ return new TextDecoder(encoding)
283
+ .decode(await stream_to_buffer(response.body));
284
+ })();
285
+ return full ? {
286
+ status: response.status,
287
+ headers: response.headers,
288
+ body: body_
289
+ }
290
+ :
291
+ body_;
255
292
  }
256
293
  /** 发起 http 请求并将响应体作为 json 解析 */
257
294
  export async function request_json(url, options) {
258
- const resp = await request(url, options);
259
- if (!resp)
295
+ const body = await request(url, options);
296
+ if (!body)
260
297
  return;
261
298
  try {
262
- return JSON.parse(resp);
299
+ return JSON.parse(body);
263
300
  }
264
301
  catch (error) {
265
- console.error(resp);
302
+ console.error(body);
266
303
  throw error;
267
304
  }
268
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.0.78",
3
+ "version": "1.0.79",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -53,9 +53,9 @@
53
53
  ]
54
54
  },
55
55
  "dependencies": {
56
- "@babel/core": "^7.23.6",
56
+ "@babel/core": "^7.23.7",
57
57
  "@babel/parser": "^7.23.6",
58
- "@babel/traverse": "^7.23.6",
58
+ "@babel/traverse": "^7.23.7",
59
59
  "@koa/cors": "^5.0.0",
60
60
  "@types/ws": "^8.5.10",
61
61
  "ali-oss": "^6.19.0",
@@ -68,20 +68,19 @@
68
68
  "colors": "^1.4.0",
69
69
  "commander": "^11.1.0",
70
70
  "emoji-regex": "^10.3.0",
71
- "fetch-cookie": "^2.1.0",
72
71
  "gulp-sort": "^2.0.0",
73
72
  "hash-string": "^1.0.0",
74
- "i18next": "^23.7.11",
73
+ "i18next": "^23.7.13",
75
74
  "i18next-scanner": "^4.4.0",
76
75
  "js-cookie": "^3.0.5",
77
- "koa": "^2.14.2",
76
+ "koa": "^2.15.0",
78
77
  "koa-compress": "^5.1.1",
79
78
  "lodash": "^4.17.21",
80
79
  "map-stream": "0.0.7",
81
80
  "mime-types": "^2.1.35",
82
- "ora": "^7.0.1",
81
+ "ora": "^8.0.1",
83
82
  "react": "^18.2.0",
84
- "react-i18next": "^13.5.0",
83
+ "react-i18next": "^14.0.0",
85
84
  "react-object-model": "^1.2.1",
86
85
  "resolve-path": "^1.4.0",
87
86
  "strip-ansi": "^7.1.0",
@@ -90,10 +89,10 @@
90
89
  "tslib": "^2.6.2",
91
90
  "typescript": "^5.3.3",
92
91
  "ua-parser-js": "2.0.0-alpha.2",
93
- "undici": "^6.0.1",
92
+ "undici": "^6.2.1",
94
93
  "vinyl": "^3.0.0",
95
94
  "vinyl-fs": "^4.0.0",
96
- "ws": "^8.15.1",
95
+ "ws": "^8.16.0",
97
96
  "xterm": "^5.3.0",
98
97
  "xterm-addon-fit": "^0.8.0",
99
98
  "xterm-addon-web-links": "^0.9.0",
@@ -103,7 +102,7 @@
103
102
  "@babel/types": "^7.23.6",
104
103
  "@types/ali-oss": "^6.16.11",
105
104
  "@types/archiver": "^6.0.2",
106
- "@types/babel__traverse": "^7.20.4",
105
+ "@types/babel__traverse": "^7.20.5",
107
106
  "@types/byte-size": "^8.1.2",
108
107
  "@types/chardet": "^0.8.3",
109
108
  "@types/gulp-sort": "2.0.4",
@@ -112,15 +111,15 @@
112
111
  "@types/koa-compress": "^4.0.6",
113
112
  "@types/lodash": "^4.14.202",
114
113
  "@types/mime-types": "^2.1.4",
115
- "@types/node": "^20.10.5",
116
- "@types/react": "^18.2.45",
114
+ "@types/node": "^20.10.6",
115
+ "@types/react": "^18.2.46",
117
116
  "@types/through2": "^2.0.41",
118
117
  "@types/tough-cookie": "^4.0.5",
119
118
  "@types/ua-parser-js": "^0.7.39",
120
119
  "@types/vinyl-fs": "^3.0.5",
121
120
  "@types/vscode": "^1.85.0",
122
- "@typescript-eslint/eslint-plugin": "^6.14.0",
123
- "@typescript-eslint/parser": "^6.14.0",
121
+ "@typescript-eslint/eslint-plugin": "^6.17.0",
122
+ "@typescript-eslint/parser": "^6.17.0",
124
123
  "eslint": "^8.56.0",
125
124
  "eslint-plugin-react": "^7.33.2",
126
125
  "eslint-plugin-xlint": "^1.0.11"
@@ -128,7 +127,7 @@
128
127
  "pnpm": {
129
128
  "patchedDependencies": {
130
129
  "@types/byte-size@8.1.2": "patches/@types__byte-size@8.1.2.patch",
131
- "koa@2.14.2": "patches/koa@2.14.2.patch"
130
+ "koa@2.15.0": "patches/koa@2.15.0.patch"
132
131
  }
133
132
  }
134
133
  }
@@ -93,9 +93,19 @@ declare global {
93
93
  interface Date {
94
94
  /** - ms?: `false` 显示到 ms */
95
95
  to_str(this: Date, ms?: boolean): string;
96
+ /** 格式化为 2024.01.01 这样的日期 */
96
97
  to_date_str(this: Date): string;
97
- /** - ms?: `false` 显示到 ms */
98
+ /** 格式化为 早上 09:00:00 这样的十二小时制可读友好的时间
99
+ - ms?: `false` 显示到 ms */
98
100
  to_time_str(this: Date, ms?: boolean): string;
101
+ /** 格式化为 17.00.00 这样的时间 */
102
+ to_dot_time_str(this: Date, ms?: boolean): string;
103
+ /** 格式化为 2024.01.01 17.00.00 这样的时间 */
104
+ to_dot_str(this: Date, ms?: boolean): string;
105
+ /** 格式化为 17:00:00 这样的时间 */
106
+ to_formal_time_str(this: Date, ms?: boolean): string;
107
+ /** 格式化为 2024.01.01 17:00:00 这样的时间 */
108
+ to_formal_str(this: Date, ms?: boolean): string;
99
109
  }
100
110
  interface Number {
101
111
  /** 12.4 KB (1 KB = 1024 B) */
@@ -316,50 +316,62 @@ Object.defineProperties(String.prototype, {
316
316
  // ------------------------------------ Date.prototype
317
317
  Object.defineProperties(Date.prototype, to_method_property_descriptors({
318
318
  to_str(ms) {
319
- const [ampm, hour] = (() => {
320
- let hour = this.getHours();
321
- if (hour <= 6)
322
- return [t('凌晨'), hour];
323
- if (hour <= 8)
324
- return [t('清晨'), hour];
325
- if (hour <= 9)
326
- return [t('早上'), hour];
327
- if (hour <= 10)
328
- return [t('上午'), hour];
329
- if (hour <= 12)
330
- return [t('中午'), hour];
331
- hour -= 12;
332
- if (hour <= 5)
333
- return [t('下午'), hour];
334
- if (hour <= 10)
335
- return [t('晚上'), hour];
336
- return [t('深夜'), hour];
337
- })();
338
- const zero_padding = { character: '0', position: 'left' };
339
- return '' +
340
- // year.month.date
341
- this.getFullYear() + '.' +
342
- String(this.getMonth() + 1).pad(2, zero_padding) + '.' +
343
- String(this.getDate()).pad(2, zero_padding) + ' ' +
344
- // 上午
345
- ampm + ' ' +
346
- // 10:03:02
347
- String(hour).pad(2, zero_padding) + ':' +
348
- String(this.getMinutes()).pad(2, zero_padding) + ':' +
349
- String(this.getSeconds()).pad(2, zero_padding) +
350
- (ms ?
351
- '.' + String(this.getMilliseconds()).pad(3, zero_padding)
352
- :
353
- '');
319
+ return `${this.to_date_str()} ${this.to_time_str(ms)}`;
354
320
  },
355
321
  to_date_str() {
356
- return this.to_str().split(' ')[0];
322
+ // 2024.01.01
323
+ return this.getFullYear() + '.' +
324
+ String(this.getMonth() + 1).padStart(2, '0') + '.' +
325
+ String(this.getDate()).padStart(2, '0');
357
326
  },
358
327
  to_time_str(ms) {
359
- const [, ampm, time] = this.to_str(ms).split(' ');
360
- return `${ampm} ${time}`;
328
+ // 早上 09:00:00
329
+ const [ampm, hour] = get_twelve_hour_clock(this);
330
+ return `${ampm} ${get_time_str(this, hour, ms, ':')}`;
361
331
  },
332
+ to_dot_time_str(ms) {
333
+ // 17.03.02
334
+ return get_time_str(this, this.getHours(), ms, '.');
335
+ },
336
+ to_dot_str(ms) {
337
+ return `${this.to_date_str()} ${this.to_dot_time_str(ms)}`;
338
+ },
339
+ to_formal_time_str(ms) {
340
+ // 17:03:02
341
+ return get_time_str(this, this.getHours(), ms, ':');
342
+ },
343
+ to_formal_str(ms) {
344
+ return `${this.to_date_str()} ${this.to_formal_time_str(ms)}`;
345
+ }
362
346
  }));
347
+ function get_twelve_hour_clock(date) {
348
+ let hour = date.getHours();
349
+ if (hour <= 6)
350
+ return [t('凌晨'), hour];
351
+ if (hour <= 8)
352
+ return [t('清晨'), hour];
353
+ if (hour <= 9)
354
+ return [t('早上'), hour];
355
+ if (hour <= 10)
356
+ return [t('上午'), hour];
357
+ if (hour <= 12)
358
+ return [t('中午'), hour];
359
+ hour -= 12;
360
+ if (hour <= 5)
361
+ return [t('下午'), hour];
362
+ if (hour <= 10)
363
+ return [t('晚上'), hour];
364
+ return [t('深夜'), hour];
365
+ }
366
+ function get_time_str(date, hour, ms, splitter) {
367
+ return String(hour).padStart(2, '0') + splitter +
368
+ String(date.getMinutes()).padStart(2, '0') + splitter +
369
+ String(date.getSeconds()).padStart(2, '0') +
370
+ (ms ?
371
+ '.' + String(date.getMilliseconds()).padStart(3, '0')
372
+ :
373
+ '');
374
+ }
363
375
  // ------------------------------------ Number.prototype
364
376
  Object.defineProperties(Number.prototype, to_method_property_descriptors({
365
377
  to_fsize_str(units = 'iec') {
package/prototype.d.ts CHANGED
@@ -116,9 +116,19 @@ declare global {
116
116
  interface Date {
117
117
  /** - ms?: `false` 显示到 ms */
118
118
  to_str(this: Date, ms?: boolean): string;
119
+ /** 格式化为 2024.01.01 这样的日期 */
119
120
  to_date_str(this: Date): string;
120
- /** - ms?: `false` 显示到 ms */
121
+ /** 格式化为 早上 09:00:00 这样的十二小时制可读友好的时间
122
+ - ms?: `false` 显示到 ms */
121
123
  to_time_str(this: Date, ms?: boolean): string;
124
+ /** 格式化为 17.00.00 这样的时间 */
125
+ to_dot_time_str(this: Date, ms?: boolean): string;
126
+ /** 格式化为 2024.01.01 17.00.00 这样的时间 */
127
+ to_dot_str(this: Date, ms?: boolean): string;
128
+ /** 格式化为 17:00:00 这样的时间 */
129
+ to_formal_time_str(this: Date, ms?: boolean): string;
130
+ /** 格式化为 2024.01.01 17:00:00 这样的时间 */
131
+ to_formal_str(this: Date, ms?: boolean): string;
122
132
  }
123
133
  interface Number {
124
134
  /** 12.4 KB (1 KB = 1024 B) */
package/prototype.js CHANGED
@@ -342,50 +342,62 @@ if (!globalThis.my_prototype_defined) {
342
342
  // ------------------------------------ Date.prototype
343
343
  Object.defineProperties(Date.prototype, to_method_property_descriptors({
344
344
  to_str(ms) {
345
- const [ampm, hour] = (() => {
346
- let hour = this.getHours();
347
- if (hour <= 6)
348
- return [t('凌晨'), hour];
349
- if (hour <= 8)
350
- return [t('清晨'), hour];
351
- if (hour <= 9)
352
- return [t('早上'), hour];
353
- if (hour <= 10)
354
- return [t('上午'), hour];
355
- if (hour <= 12)
356
- return [t('中午'), hour];
357
- hour -= 12;
358
- if (hour <= 5)
359
- return [t('下午'), hour];
360
- if (hour <= 10)
361
- return [t('晚上'), hour];
362
- return [t('深夜'), hour];
363
- })();
364
- const zero_padding = { character: '0', position: 'left' };
365
- return '' +
366
- // year.month.date
367
- this.getFullYear() + '.' +
368
- String(this.getMonth() + 1).pad(2, zero_padding) + '.' +
369
- String(this.getDate()).pad(2, zero_padding) + ' ' +
370
- // 上午
371
- ampm + ' ' +
372
- // 10:03:02
373
- String(hour).pad(2, zero_padding) + ':' +
374
- String(this.getMinutes()).pad(2, zero_padding) + ':' +
375
- String(this.getSeconds()).pad(2, zero_padding) +
376
- (ms ?
377
- '.' + String(this.getMilliseconds()).pad(3, zero_padding)
378
- :
379
- '');
345
+ return `${this.to_date_str()} ${this.to_time_str(ms)}`;
380
346
  },
381
347
  to_date_str() {
382
- return this.to_str().split(' ')[0];
348
+ // 2024.01.01
349
+ return this.getFullYear() + '.' +
350
+ String(this.getMonth() + 1).padStart(2, '0') + '.' +
351
+ String(this.getDate()).padStart(2, '0');
383
352
  },
384
353
  to_time_str(ms) {
385
- const [, ampm, time] = this.to_str(ms).split(' ');
386
- return `${ampm} ${time}`;
354
+ // 早上 09:00:00
355
+ const [ampm, hour] = get_twelve_hour_clock(this);
356
+ return `${ampm} ${get_time_str(this, hour, ms, ':')}`;
387
357
  },
358
+ to_dot_time_str(ms) {
359
+ // 17.03.02
360
+ return get_time_str(this, this.getHours(), ms, '.');
361
+ },
362
+ to_dot_str(ms) {
363
+ return `${this.to_date_str()} ${this.to_dot_time_str(ms)}`;
364
+ },
365
+ to_formal_time_str(ms) {
366
+ // 17:03:02
367
+ return get_time_str(this, this.getHours(), ms, ':');
368
+ },
369
+ to_formal_str(ms) {
370
+ return `${this.to_date_str()} ${this.to_formal_time_str(ms)}`;
371
+ }
388
372
  }));
373
+ function get_twelve_hour_clock(date) {
374
+ let hour = date.getHours();
375
+ if (hour <= 6)
376
+ return [t('凌晨'), hour];
377
+ if (hour <= 8)
378
+ return [t('清晨'), hour];
379
+ if (hour <= 9)
380
+ return [t('早上'), hour];
381
+ if (hour <= 10)
382
+ return [t('上午'), hour];
383
+ if (hour <= 12)
384
+ return [t('中午'), hour];
385
+ hour -= 12;
386
+ if (hour <= 5)
387
+ return [t('下午'), hour];
388
+ if (hour <= 10)
389
+ return [t('晚上'), hour];
390
+ return [t('深夜'), hour];
391
+ }
392
+ function get_time_str(date, hour, ms, splitter) {
393
+ return String(hour).padStart(2, '0') + splitter +
394
+ String(date.getMinutes()).padStart(2, '0') + splitter +
395
+ String(date.getSeconds()).padStart(2, '0') +
396
+ (ms ?
397
+ '.' + String(date.getMilliseconds()).padStart(3, '0')
398
+ :
399
+ '');
400
+ }
389
401
  // ------------------------------------ Number.prototype
390
402
  Object.defineProperties(Number.prototype, to_method_property_descriptors({
391
403
  to_fsize_str(units = 'iec') {
package/server.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  /// <reference types="node" resolution-mode="require"/>
5
5
  import { type Server as HttpServer, type IncomingHttpHeaders, type IncomingMessage } from 'http';
6
6
  import { type Http2SecureServer, type IncomingHttpHeaders as IncomingHttp2Headers } from 'http2';
7
- import { type Duplex } from 'stream';
7
+ import type { Duplex } from 'stream';
8
8
  import type { WebSocketServer } from 'ws';
9
9
  import type { default as Koa, Context, Next } from 'koa';
10
10
  declare module 'koa' {
@@ -18,7 +18,7 @@ declare module 'koa' {
18
18
  }
19
19
  }
20
20
  import type { UAParser } from 'ua-parser-js';
21
- import { Remote, type Headers, type RequestOptions } from './net.js';
21
+ import { Remote, type RequestOptions, type RawResponse } from './net.js';
22
22
  declare module 'http' {
23
23
  interface IncomingMessage {
24
24
  body?: Buffer;
@@ -93,7 +93,7 @@ export declare class Server {
93
93
  process_ua(ctx: Context): string;
94
94
  format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
95
95
  proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>, redirect?: RequestOptions['redirect']): Promise<void>;
96
- static filter_response_headers(headers: Headers): {};
96
+ static filter_response_headers(headers: RawResponse['headers']): Record<string, string>;
97
97
  /** 提供静态文件
98
98
  @example
99
99
  const prefix_docs = '/zh/'
package/server.js CHANGED
@@ -3,12 +3,12 @@ import { createSecureServer as http2_create_server } from 'http2';
3
3
  import { createSecureContext } from 'tls';
4
4
  import zlib from 'zlib';
5
5
  import { createReadStream } from 'fs';
6
- import { Readable } from 'stream';
6
+ import { buffer as stream_to_buffer } from 'stream/consumers';
7
7
  import util from 'util';
8
8
  // --- my libs
9
9
  import { t } from './i18n/instance.js';
10
10
  import { request as _request, Remote } from './net.js';
11
- import { stream_to_buffer, inspect, output_width, assert, range_to_numbers, encode } from './utils.js';
11
+ import { inspect, output_width, assert, range_to_numbers, encode, filter_keys } from './utils.js';
12
12
  import { flist, fread, fstat } from './file.js';
13
13
  // ------------ my server
14
14
  export class Server {
@@ -451,20 +451,20 @@ export class Server {
451
451
  response.status = response_.status;
452
452
  response.set(Server.filter_response_headers(response_.headers));
453
453
  if (response_.body)
454
- response.body = Readable.fromWeb(response_.body);
454
+ response.body = response_.body;
455
455
  }
456
456
  catch (error) {
457
457
  if (error.response?.status !== 404)
458
458
  console.log(error);
459
459
  if (error.response) {
460
- const { status, headers, text } = error.response;
460
+ const { status, headers, body } = error.response;
461
461
  if (status === 404)
462
462
  console.log(method, '404:', url);
463
463
  else
464
464
  console.log(error);
465
465
  response.status = status;
466
466
  response.set(Server.filter_response_headers(headers));
467
- response.body = text;
467
+ response.body = body;
468
468
  }
469
469
  else {
470
470
  console.log(error);
@@ -474,11 +474,7 @@ export class Server {
474
474
  }
475
475
  }
476
476
  static filter_response_headers(headers) {
477
- let headers_ = {};
478
- for (const [key, value] of headers)
479
- if (!key.startsWith(':') && !this.drop_response_headers.has(key))
480
- headers_[key] = value;
481
- return headers_;
477
+ return filter_keys(headers, key => !key.startsWith(':') && !this.drop_response_headers.has(key));
482
478
  }
483
479
  /** 提供静态文件
484
480
  @example
package/utils.d.ts CHANGED
@@ -19,8 +19,18 @@ export declare function dedent(templ: TemplateStringsArray | string, ...values:
19
19
  - selector?: 可以是 key (string) 或 (obj: any) => any
20
20
  */
21
21
  export declare function unique<T>(iterable: T[] | Iterable<T>, selector?: string | ((obj: T) => any)): any[];
22
- /** 排序对象中 key 的顺序,返回新的对象 */
23
- export declare function sort_keys<T>(obj: T): T;
22
+ /** 排序对象中 keys 的顺序,返回新的对象 */
23
+ export declare function sort_keys<TObj>(obj: TObj): TObj;
24
+ /** 映射对象中的 keys, 返回新对象 */
25
+ export declare function map_keys<TObj>(obj: TObj, mapper: (key: string) => string): TObj;
26
+ /** 映射对象中的 values, 返回新对象 */
27
+ export declare function map_values<TValue, TNewValue>(obj: {
28
+ [key: string]: TValue;
29
+ }, mapper: (value: TValue, key: string) => TNewValue): {
30
+ [k: string]: TNewValue;
31
+ };
32
+ /** 映射对象中的 keys, 返回新对象 */
33
+ export declare function filter_keys<TObj>(obj: TObj, filter: (key: string) => any): TObj;
24
34
  /** 字符串字典序比较 */
25
35
  export declare function strcmp(l: string, r: string): 0 | 1 | -1;
26
36
  /** 比较 1.10.02 这种版本号 */
@@ -126,8 +136,8 @@ export declare namespace inspect {
126
136
  export declare function map_stream<Out, In = Vinyl>(mapper: (obj: In, cb: Function) => any, options?: {
127
137
  failures?: boolean;
128
138
  }): Duplex;
129
- export declare function stream_to_buffer(stream: Readable): Promise<Buffer>;
130
139
  export declare function stream_to_lines(stream: Readable): AsyncGenerator<string, void, unknown>;
140
+ export declare function pipe_with_error(readable: Readable, transform: Transform): Transform;
131
141
  export declare class WritableMemoryStream extends Writable {
132
142
  chunks: Buffer[];
133
143
  pbuffer: Deferred<Buffer>;
package/utils.js CHANGED
@@ -77,11 +77,26 @@ export function unique(iterable, selector) {
77
77
  map.set(is_str_selector ? x[selector] : selector(x), x);
78
78
  return [...map.values()];
79
79
  }
80
- /** 排序对象中 key 的顺序,返回新的对象 */
80
+ /** 排序对象中 keys 的顺序,返回新的对象 */
81
81
  export function sort_keys(obj) {
82
82
  return Object.fromEntries(Object.entries(obj)
83
83
  .sort(([key_l], [key_r]) => strcmp(key_l, key_r)));
84
84
  }
85
+ /** 映射对象中的 keys, 返回新对象 */
86
+ export function map_keys(obj, mapper) {
87
+ return Object.fromEntries(Object.entries(obj)
88
+ .map(([key, value]) => [mapper(key), value]));
89
+ }
90
+ /** 映射对象中的 values, 返回新对象 */
91
+ export function map_values(obj, mapper) {
92
+ return Object.fromEntries(Object.entries(obj)
93
+ .map(([key, value]) => [key, mapper(value, key)]));
94
+ }
95
+ /** 映射对象中的 keys, 返回新对象 */
96
+ export function filter_keys(obj, filter) {
97
+ return Object.fromEntries(Object.entries(obj)
98
+ .filter(([key, value]) => filter(key)));
99
+ }
85
100
  /** 字符串字典序比较 */
86
101
  export function strcmp(l, r) {
87
102
  if (l === r)
@@ -444,12 +459,6 @@ export function map_stream(mapper, options) {
444
459
  }
445
460
  return stream;
446
461
  }
447
- export async function stream_to_buffer(stream) {
448
- let chunks = [];
449
- for await (const chunk of stream)
450
- chunks.push(chunk);
451
- return Buffer.concat(chunks);
452
- }
453
462
  export async function* stream_to_lines(stream) {
454
463
  let buf = '';
455
464
  for await (const chunk of stream) {
@@ -466,6 +475,11 @@ export async function* stream_to_lines(stream) {
466
475
  buf = chunk.slice(j);
467
476
  }
468
477
  }
478
+ export function pipe_with_error(readable, transform) {
479
+ // 不知道 transform 作为 AsyncIterable 使用时, emit error 是否会在 for await (...) 循环中触发错误
480
+ readable.once('error', error => { transform.emit('error', error); });
481
+ return readable.pipe(transform);
482
+ }
469
483
  export class WritableMemoryStream extends Writable {
470
484
  chunks = [];
471
485
  pbuffer = defer();
File without changes