xshell 1.0.78 → 1.0.80
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 +7 -0
- package/file.d.ts +15 -12
- package/file.js +33 -9
- package/git.d.ts +2 -1
- package/git.js +4 -1
- package/i18n/dict.json +3 -0
- package/net.browser.d.ts +14 -11
- package/net.browser.js +23 -16
- package/net.d.ts +37 -23
- package/net.js +133 -89
- package/package.json +15 -16
- package/prototype.browser.d.ts +11 -1
- package/prototype.browser.js +50 -38
- package/prototype.d.ts +11 -1
- package/prototype.js +50 -38
- package/server.d.ts +3 -3
- package/server.js +6 -10
- package/utils.browser.d.ts +1 -1
- package/utils.d.ts +14 -4
- package/utils.js +21 -7
- /package/patches/{koa@2.14.2.patch → koa@2.15.0.patch} +0 -0
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
|
|
108
|
+
export declare function fdelete(fp: string, { print }?: {
|
|
109
109
|
print?: boolean;
|
|
110
110
|
red?: boolean;
|
|
111
111
|
}): Promise<boolean>;
|
|
@@ -179,23 +179,26 @@ export interface ZipOptions {
|
|
|
179
179
|
files: boolean;
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
|
-
/**
|
|
183
|
-
返回 fp_zip 或压缩包数据
|
|
182
|
+
/** 将文件夹或文件列表压缩为 zip,返回生成的压缩包路径 (fp_zip)
|
|
184
183
|
- data:
|
|
185
|
-
- 被压缩文件夹路径 (
|
|
186
|
-
-
|
|
187
|
-
- fp_zip
|
|
188
|
-
- 传压缩包完整路径时压缩到文件, 函数返回值为 Promise<zip 路径>
|
|
189
|
-
- 传 undefined 时压缩到内存, 函数返回值为 Promise<zip Uint8Array>
|
|
184
|
+
- fpd_src: 被压缩文件夹路径 (string) 或
|
|
185
|
+
- entries: 文件列表 (Record<压缩后相对路径, 原文件绝对路径 (string) | 数据 (Uint8Array)>)
|
|
186
|
+
- fp_zip: 生成的压缩包路径 (string)
|
|
190
187
|
- options?:
|
|
191
188
|
- dirname?: `fpd_src.fname` 传入 fpd_src 才生效,修改 zip 中顶层的文件夹的名字(需要以 / 结尾),如 'web/', 为空字符串时去掉顶层文件夹,不要多一个文件夹层级 (flat)
|
|
192
189
|
- print?:
|
|
193
190
|
- info?: `true` 开始压缩、压缩完成
|
|
194
191
|
- files?: `true` 打印压缩文件列表 */
|
|
195
|
-
export declare function fzip(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
export declare function fzip(data: string | Record<string, string | Uint8Array>, fp_zip: string, options?: ZipOptions): Promise<string>;
|
|
193
|
+
/** 将文件夹或文件列表压缩为 zip,返回压缩包数据 (Uint8Array)
|
|
194
|
+
- data:
|
|
195
|
+
- fpd_src: 被压缩文件夹路径 (string) 或
|
|
196
|
+
- entries: 文件列表 (Record<压缩后相对路径, 原文件绝对路径 (string) | 数据 (Uint8Array)>)
|
|
197
|
+
- options?:
|
|
198
|
+
- print?:
|
|
199
|
+
- info?: `true` 开始压缩、压缩完成
|
|
200
|
+
- files?: `true` 打印压缩文件列表 */
|
|
201
|
+
export declare function zip(data: string | Record<string, string | Uint8Array>, options?: ZipOptions): Promise<Buffer>;
|
|
199
202
|
export declare let fwatchers: Record<string, fs.FSWatcher>;
|
|
200
203
|
/** 跟踪文本文件追加的内容,类似 tail -f */
|
|
201
204
|
export declare function ftail(fp: string, handler: (lines: string[]) => void | Promise<void>, { print }?: {
|
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
|
|
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
|
-
|
|
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) {
|
|
@@ -364,7 +362,31 @@ export async function flink(fp_real, fp_link, { junction = false, print = true }
|
|
|
364
362
|
else
|
|
365
363
|
fsp.symlink(fp_real, fp_link, is_fpd_real ? 'dir' : 'file');
|
|
366
364
|
}
|
|
367
|
-
|
|
365
|
+
/** 将文件夹或文件列表压缩为 zip,返回生成的压缩包路径 (fp_zip)
|
|
366
|
+
- data:
|
|
367
|
+
- fpd_src: 被压缩文件夹路径 (string) 或
|
|
368
|
+
- entries: 文件列表 (Record<压缩后相对路径, 原文件绝对路径 (string) | 数据 (Uint8Array)>)
|
|
369
|
+
- fp_zip: 生成的压缩包路径 (string)
|
|
370
|
+
- options?:
|
|
371
|
+
- dirname?: `fpd_src.fname` 传入 fpd_src 才生效,修改 zip 中顶层的文件夹的名字(需要以 / 结尾),如 'web/', 为空字符串时去掉顶层文件夹,不要多一个文件夹层级 (flat)
|
|
372
|
+
- print?:
|
|
373
|
+
- info?: `true` 开始压缩、压缩完成
|
|
374
|
+
- files?: `true` 打印压缩文件列表 */
|
|
375
|
+
export async function fzip(data, fp_zip, options) {
|
|
376
|
+
return _zip(data, fp_zip, options);
|
|
377
|
+
}
|
|
378
|
+
/** 将文件夹或文件列表压缩为 zip,返回压缩包数据 (Uint8Array)
|
|
379
|
+
- data:
|
|
380
|
+
- fpd_src: 被压缩文件夹路径 (string) 或
|
|
381
|
+
- entries: 文件列表 (Record<压缩后相对路径, 原文件绝对路径 (string) | 数据 (Uint8Array)>)
|
|
382
|
+
- options?:
|
|
383
|
+
- print?:
|
|
384
|
+
- info?: `true` 开始压缩、压缩完成
|
|
385
|
+
- files?: `true` 打印压缩文件列表 */
|
|
386
|
+
export async function zip(data, options) {
|
|
387
|
+
return _zip(data, undefined, options);
|
|
388
|
+
}
|
|
389
|
+
async function _zip(data, fp_zip, { dirname, print = { files: true, info: true } } = {}) {
|
|
368
390
|
let entries;
|
|
369
391
|
let fpd_src;
|
|
370
392
|
if (typeof data === 'string') {
|
|
@@ -378,12 +400,14 @@ export async function fzip(data, fp_zip, { dirname, print = { files: true, info:
|
|
|
378
400
|
entries = Object.fromEntries((await flist(fpd_src, { print: false }))
|
|
379
401
|
.map(fp => ([dirname + fp, fpd_src + fp])));
|
|
380
402
|
}
|
|
381
|
-
else
|
|
403
|
+
else {
|
|
404
|
+
assert(!dirname, 'dirname 在传入 fpd_src 时才生效');
|
|
382
405
|
entries = data;
|
|
406
|
+
}
|
|
383
407
|
if (print.info)
|
|
384
|
-
console.log(`开始压缩${fpd_src ? ` ${fpd_src}` : '文件索引'} -> ${fp_zip ? `${fp_zip}/${dirname}` : '内存'}`);
|
|
408
|
+
console.log(`开始压缩${fpd_src ? ` ${fpd_src}` : '文件索引'} -> ${fp_zip ? `${fp_zip}/${dirname || '{entries}'}` : '内存'}`);
|
|
385
409
|
const { default: archiver } = await import('archiver');
|
|
386
|
-
let archive = archiver('zip'
|
|
410
|
+
let archive = archiver('zip');
|
|
387
411
|
let ostream = fp_zip ?
|
|
388
412
|
fs.createWriteStream(fp_zip, { highWaterMark: 16 * 2 ** 20 /* 16 MB */ })
|
|
389
413
|
:
|
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
|
-
|
|
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
|
-
|
|
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
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:
|
|
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,
|
|
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(),
|
|
65
|
+
assert(key === key.toLowerCase(), `传入 request 的 headers 参数中 key 应该都是小写的,实际为 ${key}`);
|
|
65
66
|
headers.set(key, _headers[key]);
|
|
66
67
|
}
|
|
67
|
-
let
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
42
|
-
body?: string | Record<string, any> |
|
|
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:
|
|
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
|
|
76
|
-
- body?: http
|
|
77
|
-
|
|
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' 时返回
|
|
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<
|
|
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<
|
|
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,30 +36,31 @@ const drop_request_headers = new Set([
|
|
|
48
36
|
'upgrade',
|
|
49
37
|
]);
|
|
50
38
|
let proxy_agents = {};
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
43
|
+
return await undici_request(url, options);
|
|
60
44
|
}
|
|
61
45
|
catch (error) {
|
|
62
|
-
if (count
|
|
63
|
-
error.name !== 'TimeoutError' && !['ECONNRESET', 'ETIMEDOUT', 'ESOCKETTIMEDOUT'].includes(error.cause?.code))
|
|
64
|
-
throw error;
|
|
65
|
-
else {
|
|
46
|
+
if (count < retries && (error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
66
47
|
const duration = 2 ** count;
|
|
67
48
|
console.log(`${t('等待 {{duration}} 秒后重试 fetch ({{_count}}) …', { duration, _count: count }).yellow} ${url.toString().blue.underline}`);
|
|
68
49
|
await delay(1000 * duration);
|
|
69
|
-
return
|
|
50
|
+
return request_retry(url, options, timeout, retries, count + 1);
|
|
70
51
|
}
|
|
52
|
+
else if (error.name === 'AbortError' || error.name === 'TimeoutError')
|
|
53
|
+
throw Object.assign(new Error(`request 超时: ${url.toString()}`), { name: 'TimeoutError' });
|
|
54
|
+
else
|
|
55
|
+
throw error;
|
|
71
56
|
}
|
|
72
57
|
}
|
|
73
|
-
export async function request(url,
|
|
74
|
-
const {
|
|
58
|
+
export async function request(url, options = {}) {
|
|
59
|
+
const { ProxyAgent, FormData } = await import('undici');
|
|
60
|
+
const { Cookie } = await import('tough-cookie');
|
|
61
|
+
await cookies.init();
|
|
62
|
+
const { queries, headers: _headers, body, type = 'application/json', timeout = 5 * 1000, auth, cookies: _cookies, raw = false, full = false, redirect = 'follow', } = options;
|
|
63
|
+
let { method, retries, encoding, proxy, } = options;
|
|
75
64
|
url = new URL(url);
|
|
76
65
|
if (queries)
|
|
77
66
|
for (const key in queries) {
|
|
@@ -80,38 +69,46 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
80
69
|
value = value ? '1' : '0';
|
|
81
70
|
url.searchParams.append(key, value);
|
|
82
71
|
}
|
|
72
|
+
const urlstr = url.toString();
|
|
83
73
|
if (body !== undefined && !method)
|
|
84
74
|
method = 'POST';
|
|
85
75
|
if (retries === true)
|
|
86
76
|
retries = 2;
|
|
87
77
|
// --- headers, http/2 开始都用小写的 headers
|
|
88
|
-
let headers =
|
|
78
|
+
let headers = {
|
|
89
79
|
'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
|
-
'
|
|
80
|
+
'accept-encoding': 'gzip, deflate, br',
|
|
81
|
+
'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
82
|
'sec-ch-ua-platform': '"Windows"',
|
|
92
83
|
'sec-ch-ua-platform-version': '"15.0.0"',
|
|
93
|
-
}
|
|
84
|
+
};
|
|
94
85
|
if (body !== undefined)
|
|
95
|
-
headers
|
|
86
|
+
headers['content-type'] = type;
|
|
96
87
|
if (auth)
|
|
97
|
-
headers.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
88
|
+
headers.authorization = auth.type === 'basic' ?
|
|
89
|
+
`Basic ${`${auth.username}:${auth.password}`.to_base64()}`
|
|
90
|
+
:
|
|
91
|
+
`Bearer ${auth.token}`;
|
|
92
|
+
const request_cookies = unique([
|
|
93
|
+
...cookies.jar.getCookiesSync(urlstr),
|
|
94
|
+
..._cookies ?
|
|
95
|
+
Object.entries(_cookies).map(([key, value]) => new Cookie({ key, value }))
|
|
96
|
+
:
|
|
97
|
+
[]
|
|
98
|
+
], 'key');
|
|
99
|
+
if (request_cookies.length)
|
|
100
|
+
headers.cookie = request_cookies.map(cookie => cookie.cookieString())
|
|
101
|
+
.join('; ');
|
|
102
|
+
if (_headers) {
|
|
103
|
+
assert(Object.getPrototypeOf(_headers)?.constructor.name !== 'Headers');
|
|
104
|
+
for (const key in _headers)
|
|
105
|
+
// 可能在 http/2 的 response 中会有这样开头的保留 headers, 在透传时忽略比较好
|
|
106
|
+
if (!key.startsWith(':') && !key.startsWith('sec-') && !drop_request_headers.has(key)) {
|
|
107
|
+
assert(key === key.toLowerCase(), t('传入 request 的 headers 参数中 key 应该都是小写的,实际为 {{key}}', { key }));
|
|
108
|
+
headers[key] = _headers[key];
|
|
107
109
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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 = {
|
|
110
|
+
}
|
|
111
|
+
let undici_options = {
|
|
115
112
|
...method ? { method } : {},
|
|
116
113
|
dispatcher: (() => {
|
|
117
114
|
if (proxy) {
|
|
@@ -120,27 +117,29 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
120
117
|
return proxy_agents[proxy] ??= new ProxyAgent({ uri: proxy });
|
|
121
118
|
}
|
|
122
119
|
})(),
|
|
123
|
-
|
|
124
|
-
redirect,
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
// todo: 强制手动处理重定向,来正确处理 cookie ?
|
|
121
|
+
maxRedirections: redirect === 'follow' ? 5 : 0,
|
|
122
|
+
// 下面这些 timeout 都不是总的时间,没法用
|
|
123
|
+
// headersTimeout: timeout - 1000,
|
|
124
|
+
// 从收完 headers 开始算
|
|
125
|
+
// bodyTimeout: timeout - 1000,
|
|
126
|
+
// @ts-ignore 没有类型声明,实际可用
|
|
127
|
+
// connectTimeout: timeout - 1000,
|
|
127
128
|
headers,
|
|
128
129
|
// --- body
|
|
129
130
|
body: (() => {
|
|
130
131
|
if (body === undefined)
|
|
131
132
|
return;
|
|
132
133
|
switch (type) {
|
|
133
|
-
case 'application/json':
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
:
|
|
141
|
-
JSON.stringify(body);
|
|
134
|
+
case 'application/json': // 可能的类型 string | Record<string, any> | Uint8Array
|
|
135
|
+
if (typeof body === 'string')
|
|
136
|
+
return body;
|
|
137
|
+
if (body instanceof Uint8Array)
|
|
138
|
+
return body;
|
|
139
|
+
assert(!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)));
|
|
140
|
+
return JSON.stringify(body);
|
|
142
141
|
case 'application/x-www-form-urlencoded':
|
|
143
|
-
return body instanceof URLSearchParams ? body : new URLSearchParams(body);
|
|
142
|
+
return (body instanceof URLSearchParams ? body : new URLSearchParams(body)).toString();
|
|
144
143
|
case 'multipart/form-data':
|
|
145
144
|
if (body instanceof FormData)
|
|
146
145
|
return body;
|
|
@@ -157,29 +156,64 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
157
156
|
};
|
|
158
157
|
let response;
|
|
159
158
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
const { statusCode: status, headers: _headers, body: _body } = await request_retry(url, undici_options, timeout, retries);
|
|
160
|
+
// 处理 cookie,自动保存到 cookie jar
|
|
161
|
+
let _cookies = _headers['set-cookie'] || [];
|
|
162
|
+
if (typeof _cookies === 'string')
|
|
163
|
+
_cookies = [_cookies];
|
|
164
|
+
_cookies.map(cookie => cookies.jar.setCookieSync(cookie, urlstr, { ignoreError: true }));
|
|
165
|
+
const headers = map_values(_headers, value => Array.isArray(value) ? value.join(', ') : (value || ''));
|
|
166
|
+
// UndiciResponse.body 没有自动根据 content-encoding 来解码,这里手动处理并替换 body 为解码后的 stream
|
|
167
|
+
let body = _body;
|
|
168
|
+
const content_encoding = headers['content-encoding']?.trim().toLowerCase();
|
|
169
|
+
if (content_encoding) {
|
|
170
|
+
assert(!content_encoding.includes(','));
|
|
171
|
+
switch (content_encoding) {
|
|
172
|
+
case 'gzip':
|
|
173
|
+
case 'x-gzip':
|
|
174
|
+
body = pipe_with_error(_body, zlib.createGunzip({
|
|
175
|
+
// Be less strict when decoding compressed responses, since sometimes
|
|
176
|
+
// servers send slightly invalid responses that are still accepted
|
|
177
|
+
// by common browsers.
|
|
178
|
+
// Always using Z_SYNC_FLUSH is what cURL does.
|
|
179
|
+
flush: zlib.constants.Z_SYNC_FLUSH,
|
|
180
|
+
finishFlush: zlib.constants.Z_SYNC_FLUSH
|
|
181
|
+
}));
|
|
182
|
+
break;
|
|
183
|
+
case 'deflate':
|
|
184
|
+
body = pipe_with_error(_body, zlib.createInflate());
|
|
185
|
+
break;
|
|
186
|
+
case 'br':
|
|
187
|
+
body = pipe_with_error(_body, zlib.createBrotliDecompress());
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
throw new Error(t('不支持 content-encoding: {{encoding}} 的 http 请求', { encoding: content_encoding.quote() }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
response = {
|
|
194
|
+
status,
|
|
195
|
+
headers,
|
|
196
|
+
body
|
|
197
|
+
};
|
|
198
|
+
if (!((200 <= status && status <= 299) || (redirect === 'manual' && 300 <= status && status < 400)))
|
|
162
199
|
throw Object.assign(new Error(t('状态码 {{status}} 非 2xx', { status: response.status })), { name: 'StatusCodeError' });
|
|
163
200
|
}
|
|
164
201
|
catch (error) {
|
|
202
|
+
;
|
|
165
203
|
error.url = url;
|
|
166
204
|
error.options = options;
|
|
167
205
|
if (response)
|
|
168
206
|
error.response = {
|
|
169
207
|
status: response.status,
|
|
170
|
-
url: response.url,
|
|
171
208
|
headers: response.headers,
|
|
172
|
-
|
|
173
|
-
type: response.type,
|
|
174
|
-
text: await response.text(),
|
|
175
|
-
redirected: response.redirected
|
|
209
|
+
body: await stream_to_text(response.body),
|
|
176
210
|
};
|
|
177
211
|
error[inspect.custom] = (depth, options, inspect) => {
|
|
178
212
|
const { colors } = options;
|
|
179
213
|
let _method = method || 'GET';
|
|
180
214
|
if (colors)
|
|
181
215
|
_method = _method.red;
|
|
182
|
-
let _url =
|
|
216
|
+
let _url = urlstr;
|
|
183
217
|
if (colors)
|
|
184
218
|
_url = _url.blue.underline;
|
|
185
219
|
let s = '\n' +
|
|
@@ -218,14 +252,16 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
218
252
|
if (colors)
|
|
219
253
|
_headers = _headers.yellow;
|
|
220
254
|
s += _headers + '\n';
|
|
221
|
-
for (const
|
|
255
|
+
for (const key in response.headers) {
|
|
256
|
+
const value = response.headers[key];
|
|
222
257
|
s += `${key}: ${value}\n`;
|
|
223
|
-
|
|
258
|
+
}
|
|
259
|
+
if (error.response.body) {
|
|
224
260
|
let _body = t('响应体:');
|
|
225
261
|
if (colors)
|
|
226
262
|
_body = _body.yellow;
|
|
227
263
|
s += _body + '\n' +
|
|
228
|
-
error.response.
|
|
264
|
+
error.response.body + '\n';
|
|
229
265
|
}
|
|
230
266
|
}
|
|
231
267
|
let _stack = t('调用栈:');
|
|
@@ -242,27 +278,35 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
242
278
|
}
|
|
243
279
|
if (raw)
|
|
244
280
|
return response;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
const body_ = await (async () => {
|
|
282
|
+
if (!response.body)
|
|
283
|
+
return encoding === 'binary' ? Buffer.from([]) : '';
|
|
284
|
+
if (encoding === 'binary')
|
|
285
|
+
return stream_to_buffer(response.body);
|
|
286
|
+
encoding ||= /charset=(.*)/.exec(response.headers['content-type'])?.[1] || 'utf-8';
|
|
287
|
+
if (/utf-?8/i.test(encoding))
|
|
288
|
+
return stream_to_text(response.body);
|
|
289
|
+
return new TextDecoder(encoding)
|
|
290
|
+
.decode(await stream_to_buffer(response.body));
|
|
291
|
+
})();
|
|
292
|
+
return full ? {
|
|
293
|
+
status: response.status,
|
|
294
|
+
headers: response.headers,
|
|
295
|
+
body: body_
|
|
296
|
+
}
|
|
297
|
+
:
|
|
298
|
+
body_;
|
|
255
299
|
}
|
|
256
300
|
/** 发起 http 请求并将响应体作为 json 解析 */
|
|
257
301
|
export async function request_json(url, options) {
|
|
258
|
-
const
|
|
259
|
-
if (!
|
|
302
|
+
const body = await request(url, options);
|
|
303
|
+
if (!body)
|
|
260
304
|
return;
|
|
261
305
|
try {
|
|
262
|
-
return JSON.parse(
|
|
306
|
+
return JSON.parse(body);
|
|
263
307
|
}
|
|
264
308
|
catch (error) {
|
|
265
|
-
console.error(
|
|
309
|
+
console.error(body);
|
|
266
310
|
throw error;
|
|
267
311
|
}
|
|
268
312
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
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.
|
|
56
|
+
"@babel/core": "^7.23.7",
|
|
57
57
|
"@babel/parser": "^7.23.6",
|
|
58
|
-
"@babel/traverse": "^7.23.
|
|
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.
|
|
73
|
+
"i18next": "^23.7.13",
|
|
75
74
|
"i18next-scanner": "^4.4.0",
|
|
76
75
|
"js-cookie": "^3.0.5",
|
|
77
|
-
"koa": "^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": "^
|
|
81
|
+
"ora": "^8.0.1",
|
|
83
82
|
"react": "^18.2.0",
|
|
84
|
-
"react-i18next": "^
|
|
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.
|
|
92
|
+
"undici": "^6.2.1",
|
|
94
93
|
"vinyl": "^3.0.0",
|
|
95
94
|
"vinyl-fs": "^4.0.0",
|
|
96
|
-
"ws": "^8.
|
|
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.
|
|
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.
|
|
116
|
-
"@types/react": "^18.2.
|
|
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.
|
|
123
|
-
"@typescript-eslint/parser": "^6.
|
|
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.
|
|
130
|
+
"koa@2.15.0": "patches/koa@2.15.0.patch"
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
}
|
package/prototype.browser.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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) */
|
package/prototype.browser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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 {
|
|
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
|
|
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:
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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.browser.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export declare function encode(str: string): Uint8Array;
|
|
|
56
56
|
在流式处理 (buffer 可能不完整) 时,应使用独立的 TextDecoder 实例调用 decode(buffer, { stream: true }) */
|
|
57
57
|
export declare function decode(buffer: Uint8Array): string;
|
|
58
58
|
/** 字符串字典序比较 */
|
|
59
|
-
export declare function strcmp(l: string, r: string):
|
|
59
|
+
export declare function strcmp(l: string, r: string): 1 | 0 | -1;
|
|
60
60
|
/** 比较 1.10.02 这种版本号 */
|
|
61
61
|
export declare function vercmp(l: string, r: string): number;
|
|
62
62
|
export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
|
package/utils.d.ts
CHANGED
|
@@ -19,10 +19,20 @@ 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
|
-
/** 排序对象中
|
|
23
|
-
export declare function sort_keys<
|
|
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
|
-
export declare function strcmp(l: string, r: string):
|
|
35
|
+
export declare function strcmp(l: string, r: string): 1 | 0 | -1;
|
|
26
36
|
/** 比较 1.10.02 这种版本号 */
|
|
27
37
|
export declare function vercmp(l: string, r: string): number;
|
|
28
38
|
export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
|
|
@@ -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
|
-
/** 排序对象中
|
|
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
|