xshell 1.0.104 → 1.0.106
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.d.ts +4 -4
- package/Terminal.js +6 -6
- package/git.d.ts +0 -18
- package/git.js +0 -52
- package/i18n/README.md +0 -1
- package/i18n/index.js +1 -3
- package/package.json +15 -17
- package/process.d.ts +2 -2
- package/process.js +5 -9
- package/prototype.browser.d.ts +8 -4
- package/prototype.browser.js +12 -8
- package/prototype.d.ts +8 -4
- package/prototype.js +12 -8
- package/server.d.ts +2 -1
- package/server.js +20 -12
- package/utils.d.ts +4 -2
- package/utils.js +18 -12
package/Terminal.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import 'xterm/css/xterm.css';
|
|
2
|
-
import { Terminal as
|
|
3
|
-
import { FitAddon } from 'xterm
|
|
1
|
+
import '@xterm/xterm/css/xterm.css';
|
|
2
|
+
import { Terminal as XTerminal } from '@xterm/xterm';
|
|
3
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
4
4
|
import { Model } from 'react-object-model';
|
|
5
5
|
import type { Remote } from './net.browser.js';
|
|
6
6
|
export declare function Terminal({ font }: {
|
|
7
7
|
font: string;
|
|
8
8
|
}): import("react/jsx-runtime").JSX.Element;
|
|
9
9
|
declare class TerminalModel extends Model<TerminalModel> {
|
|
10
|
-
term:
|
|
10
|
+
term: XTerminal;
|
|
11
11
|
fit_addon: FitAddon;
|
|
12
12
|
stdio_id: number;
|
|
13
13
|
fit(): void;
|
package/Terminal.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import 'xterm/css/xterm.css';
|
|
2
|
+
import '@xterm/xterm/css/xterm.css';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
|
-
import { Terminal as
|
|
5
|
-
import { FitAddon } from 'xterm
|
|
6
|
-
import { WebglAddon } from 'xterm
|
|
7
|
-
import { WebLinksAddon } from 'xterm
|
|
4
|
+
import { Terminal as XTerminal } from '@xterm/xterm';
|
|
5
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
6
|
+
import { WebglAddon } from '@xterm/addon-webgl';
|
|
7
|
+
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
8
8
|
// 没有 ui
|
|
9
9
|
// import { SearchAddon } from 'xterm-addon-search'
|
|
10
10
|
import { Model } from 'react-object-model';
|
|
@@ -14,7 +14,7 @@ export function Terminal({ font }) {
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
(async () => {
|
|
16
16
|
await document.fonts.ready;
|
|
17
|
-
let term = new
|
|
17
|
+
let term = new XTerminal({
|
|
18
18
|
fontFamily: font,
|
|
19
19
|
fontSize: 16,
|
|
20
20
|
cursorStyle: 'bar',
|
package/git.d.ts
CHANGED
|
@@ -33,22 +33,4 @@ 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
|
-
/** 返回是否进行了修改 */
|
|
37
|
-
merge(branch: string, fast_forward?: boolean): Promise<boolean>;
|
|
38
|
-
/** 记住当前分支名
|
|
39
|
-
checkout 到 <目标分支>
|
|
40
|
-
merge 当前分支 */
|
|
41
|
-
merge_into(target_branch?: string): Promise<void>;
|
|
42
|
-
/** 1. fetch 远程修改
|
|
43
|
-
2. checkout `<branch>` 分支
|
|
44
|
-
3. merge `<remote>/<branch>`
|
|
45
|
-
|
|
46
|
-
执行前需要保证工作目录 clean
|
|
47
|
-
|
|
48
|
-
- branch?: `'main'` */
|
|
49
|
-
update(branch?: 'main'): Promise<void>;
|
|
50
|
-
update(branch: string): Promise<void>;
|
|
51
|
-
update(branch: string, remote?: string): Promise<void>;
|
|
52
|
-
/** - message?: stash description */
|
|
53
|
-
stash(message?: string): Promise<import("./process.js").CallResult<string>>;
|
|
54
36
|
}
|
package/git.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { assert } from './utils.js';
|
|
2
1
|
import { call } from './process.js';
|
|
3
2
|
import { fread, fmkdir } from './file.js';
|
|
4
3
|
export class Git {
|
|
@@ -121,56 +120,5 @@ export class Git {
|
|
|
121
120
|
throw error;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
/** 返回是否进行了修改 */
|
|
125
|
-
async merge(branch, fast_forward = true) {
|
|
126
|
-
console.log(`合并 ${branch} 分支`);
|
|
127
|
-
try {
|
|
128
|
-
const { stdout, stderr } = await this.call([
|
|
129
|
-
'merge',
|
|
130
|
-
branch,
|
|
131
|
-
...fast_forward ? [] : ['--no-ff']
|
|
132
|
-
], { print: false });
|
|
133
|
-
const unchanged = stdout === 'Already up to date.\n';
|
|
134
|
-
if (unchanged)
|
|
135
|
-
console.log('当前分支无需更新');
|
|
136
|
-
else
|
|
137
|
-
console.log(stdout.trimEnd());
|
|
138
|
-
if (stderr)
|
|
139
|
-
console.log(stderr);
|
|
140
|
-
return !unchanged;
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
if (error.result) {
|
|
144
|
-
const { stdout, stderr } = error.result;
|
|
145
|
-
if (stdout)
|
|
146
|
-
console.log(stdout);
|
|
147
|
-
if (stderr)
|
|
148
|
-
console.log(stderr);
|
|
149
|
-
}
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/** 记住当前分支名
|
|
154
|
-
checkout 到 <目标分支>
|
|
155
|
-
merge 当前分支 */
|
|
156
|
-
async merge_into(target_branch = 'main') {
|
|
157
|
-
const branch = await this.get_branch();
|
|
158
|
-
assert(branch !== target_branch);
|
|
159
|
-
await this.checkout(target_branch);
|
|
160
|
-
await this.merge(branch, true);
|
|
161
|
-
}
|
|
162
|
-
async update(branch = 'main', remote) {
|
|
163
|
-
await this.fetch({ remote });
|
|
164
|
-
await this.checkout(branch);
|
|
165
|
-
await this.merge(`${remote || 'origin'}/${branch}`);
|
|
166
|
-
}
|
|
167
|
-
/** - message?: stash description */
|
|
168
|
-
async stash(message) {
|
|
169
|
-
return this.call([
|
|
170
|
-
'stash',
|
|
171
|
-
'push',
|
|
172
|
-
...message ? ['--message', message] : [],
|
|
173
|
-
]);
|
|
174
|
-
}
|
|
175
123
|
}
|
|
176
124
|
//# sourceMappingURL=git.js.map
|
package/i18n/README.md
CHANGED
package/i18n/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import Cookies from 'js-cookie';
|
|
2
1
|
import { default as i18next } from 'i18next';
|
|
3
2
|
import { Dict } from './dict.js';
|
|
4
3
|
export const LANGUAGES = ['zh', 'en', 'ja', 'ko'];
|
|
@@ -35,8 +34,7 @@ export class I18N {
|
|
|
35
34
|
const dict = new Dict(_dict);
|
|
36
35
|
if (!language && is_browser)
|
|
37
36
|
language = (new URLSearchParams(location.search).get('language') ||
|
|
38
|
-
window.language
|
|
39
|
-
Cookies.get('language'));
|
|
37
|
+
window.language);
|
|
40
38
|
if (!language)
|
|
41
39
|
language = Intl.DateTimeFormat().resolvedOptions().locale.slice(0, 2);
|
|
42
40
|
if (!I18N.LANGUAGE_REGEXP.test(language)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.106",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"interactive programming"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=22.2.0",
|
|
20
20
|
"vscode": ">=1.81.0"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
@@ -58,9 +58,13 @@
|
|
|
58
58
|
"@babel/traverse": "^7.24.5",
|
|
59
59
|
"@koa/cors": "^5.0.0",
|
|
60
60
|
"@types/ws": "^8.5.10",
|
|
61
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
62
|
-
"@typescript-eslint/parser": "^7.
|
|
63
|
-
"@typescript-eslint/utils": "^7.
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "^7.10.0",
|
|
62
|
+
"@typescript-eslint/parser": "^7.10.0",
|
|
63
|
+
"@typescript-eslint/utils": "^7.10.0",
|
|
64
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
65
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
66
|
+
"@xterm/addon-webgl": "^0.18.0",
|
|
67
|
+
"@xterm/xterm": "^5.5.0",
|
|
64
68
|
"ali-oss": "^6.20.0",
|
|
65
69
|
"archiver": "^7.0.1",
|
|
66
70
|
"byte-size": "^8.1.1",
|
|
@@ -69,17 +73,16 @@
|
|
|
69
73
|
"cli-table3": "^0.6.5",
|
|
70
74
|
"cli-truncate": "^4.0.0",
|
|
71
75
|
"colors": "^1.4.0",
|
|
72
|
-
"commander": "^12.
|
|
76
|
+
"commander": "^12.1.0",
|
|
73
77
|
"emoji-regex": "^10.3.0",
|
|
74
|
-
"eslint": "^9.
|
|
78
|
+
"eslint": "^9.3.0",
|
|
75
79
|
"eslint-plugin-import": "^2.29.1",
|
|
76
80
|
"eslint-plugin-react": "^7.34.1",
|
|
77
81
|
"gulp-sort": "^2.0.0",
|
|
78
82
|
"hash-string": "^1.0.0",
|
|
79
83
|
"https-proxy-agent": "^7.0.4",
|
|
80
|
-
"i18next": "^23.11.
|
|
84
|
+
"i18next": "^23.11.5",
|
|
81
85
|
"i18next-scanner": "^4.4.0",
|
|
82
|
-
"js-cookie": "^3.0.5",
|
|
83
86
|
"koa": "^2.15.3",
|
|
84
87
|
"koa-compress": "^5.1.1",
|
|
85
88
|
"lodash": "^4.17.21",
|
|
@@ -96,14 +99,10 @@
|
|
|
96
99
|
"tslib": "^2.6.2",
|
|
97
100
|
"typescript": "^5.4.5",
|
|
98
101
|
"ua-parser-js": "^2.0.0-beta.2",
|
|
99
|
-
"undici": "^6.
|
|
102
|
+
"undici": "^6.18.0",
|
|
100
103
|
"vinyl": "^3.0.0",
|
|
101
104
|
"vinyl-fs": "^4.0.0",
|
|
102
|
-
"ws": "^8.17.0"
|
|
103
|
-
"xterm": "^5.3.0",
|
|
104
|
-
"xterm-addon-fit": "^0.8.0",
|
|
105
|
-
"xterm-addon-web-links": "^0.9.0",
|
|
106
|
-
"xterm-addon-webgl": "^0.16.0"
|
|
105
|
+
"ws": "^8.17.0"
|
|
107
106
|
},
|
|
108
107
|
"devDependencies": {
|
|
109
108
|
"@babel/types": "^7.24.5",
|
|
@@ -115,10 +114,9 @@
|
|
|
115
114
|
"@types/eslint": "^8.56.10",
|
|
116
115
|
"@types/estree": "^1.0.5",
|
|
117
116
|
"@types/gulp-sort": "^2.0.4",
|
|
118
|
-
"@types/js-cookie": "^3.0.6",
|
|
119
117
|
"@types/koa": "^2.15.0",
|
|
120
118
|
"@types/koa-compress": "^4.0.6",
|
|
121
|
-
"@types/lodash": "^4.17.
|
|
119
|
+
"@types/lodash": "^4.17.4",
|
|
122
120
|
"@types/mime-types": "^2.1.4",
|
|
123
121
|
"@types/node": "^20.12.12",
|
|
124
122
|
"@types/react": "^18.3.2",
|
package/process.d.ts
CHANGED
|
@@ -135,6 +135,6 @@ export interface TermNodeOptions extends TermOptions {
|
|
|
135
135
|
/** `false` --trace-warnings */
|
|
136
136
|
trace_warnings?: boolean;
|
|
137
137
|
}
|
|
138
|
-
/** 在 term tab 中创建 node.exe 进程
|
|
139
|
-
export declare function term_nodejs(fp_js: string, args?: string[], { title, inspect, tsnode, break: _break, resolve, trace_warnings, ...
|
|
138
|
+
/** 在 term tab 中创建 node.exe 进程 */
|
|
139
|
+
export declare function term_nodejs(fp_js: string, args?: string[], { title, inspect, tsnode, break: _break, resolve, trace_warnings, ...options }?: TermNodeOptions): Promise<ChildProcess>;
|
|
140
140
|
export {};
|
package/process.js
CHANGED
|
@@ -178,9 +178,7 @@ export async function call(exe, args = [], options = {}) {
|
|
|
178
178
|
- throw_code?: `true` code 不为 0 时是否抛出异常 whether to throw Error when code is not 0 */
|
|
179
179
|
export async function call_nodejs(js, args = [], { inspect, break: _break, ...options } = {}) {
|
|
180
180
|
return call(exe_nodejs, [
|
|
181
|
-
...inspect ? [
|
|
182
|
-
_break ? `--inspect-brk=localhost:${inspect}` : `--inspect=localhost:${inspect}`
|
|
183
|
-
] : [],
|
|
181
|
+
...inspect ? [`--inspect${_break ? '-brk' : ''}=localhost:${inspect}`] : [],
|
|
184
182
|
js,
|
|
185
183
|
...args
|
|
186
184
|
], options);
|
|
@@ -204,15 +202,13 @@ export async function term(exe, args = [], { cwd = process.cwd().fpd, print = tr
|
|
|
204
202
|
envs
|
|
205
203
|
});
|
|
206
204
|
}
|
|
207
|
-
/** 在 term tab 中创建 node.exe 进程
|
|
208
|
-
export async function term_nodejs(fp_js, args = [], { title, inspect, tsnode = false, break: _break, resolve = false, trace_warnings = false, ...
|
|
205
|
+
/** 在 term tab 中创建 node.exe 进程 */
|
|
206
|
+
export async function term_nodejs(fp_js, args = [], { title, inspect, tsnode = false, break: _break, resolve = false, trace_warnings = false, ...options } = {}) {
|
|
209
207
|
return term(exe_nodejs, [
|
|
210
208
|
...trace_warnings ? ['--trace-warnings'] : [],
|
|
211
209
|
...resolve ? ['--experimental-specifier-resolution=node'] : [],
|
|
212
210
|
...title ? [`--title=${title}`] : [],
|
|
213
|
-
...inspect ? [
|
|
214
|
-
_break ? `--inspect-brk=localhost:${inspect}` : `--inspect=localhost:${inspect}`
|
|
215
|
-
] : [],
|
|
211
|
+
...inspect ? [`--inspect${_break ? '-brk' : ''}=localhost:${inspect}`] : [],
|
|
216
212
|
...tsnode ? [
|
|
217
213
|
'--loader=ts-node/esm',
|
|
218
214
|
'--experimental-specifier-resolution=node'
|
|
@@ -221,7 +217,7 @@ export async function term_nodejs(fp_js, args = [], { title, inspect, tsnode = f
|
|
|
221
217
|
...args
|
|
222
218
|
], {
|
|
223
219
|
title,
|
|
224
|
-
...
|
|
220
|
+
...options,
|
|
225
221
|
});
|
|
226
222
|
}
|
|
227
223
|
const short_exe_names = {
|
package/prototype.browser.d.ts
CHANGED
|
@@ -75,10 +75,14 @@ declare global {
|
|
|
75
75
|
text: string;
|
|
76
76
|
};
|
|
77
77
|
space(this: string): string;
|
|
78
|
-
/**
|
|
79
|
-
strip_start(this: string, prefix: string): string;
|
|
80
|
-
/**
|
|
81
|
-
|
|
78
|
+
/** 返回去掉 prefix 开头的字符串,可选 validate = true 确保字符串以 prefix 开头(失败时抛出错误) */
|
|
79
|
+
strip_start(this: string, prefix: string, validate?: boolean): string;
|
|
80
|
+
/** 返回去掉 prefix 开头的字符串,如果没有以 prefix 开头则返回原字符串 */
|
|
81
|
+
strip_if_start(this: string, prefix: string): string;
|
|
82
|
+
/** 返回去掉 suffix 结尾的字符串,可选 validate = true 确保字符串以 suffix 结尾(失败时抛出错误) */
|
|
83
|
+
strip_end(this: string, suffix: string, validate?: boolean): string;
|
|
84
|
+
/** 返回去掉 suffix 结尾的字符串,如果没有以 suffix 结尾则返回原字符串 */
|
|
85
|
+
strip_if_end(this: string, suffix: string): string;
|
|
82
86
|
/** 等价于 .endsWith('/') */
|
|
83
87
|
isdir: boolean;
|
|
84
88
|
/** 以 `/` 分割的路径,可能以 / 结尾 */
|
package/prototype.browser.js
CHANGED
|
@@ -259,17 +259,21 @@ Object.defineProperties(String.prototype, {
|
|
|
259
259
|
text: this.slice(i)
|
|
260
260
|
};
|
|
261
261
|
},
|
|
262
|
-
strip_start(prefix) {
|
|
263
|
-
if (this.startsWith(prefix))
|
|
264
|
-
return this.slice(prefix.length);
|
|
265
|
-
else
|
|
262
|
+
strip_start(prefix, validate) {
|
|
263
|
+
if (validate && !this.startsWith(prefix))
|
|
266
264
|
throw new Error(`字符串没有以前缀 ${prefix} 开头: ${this}`);
|
|
265
|
+
return this.slice(prefix.length);
|
|
267
266
|
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
strip_if_start(prefix) {
|
|
268
|
+
return this.startsWith(prefix) ? this.slice(prefix.length) : this;
|
|
269
|
+
},
|
|
270
|
+
strip_end(suffix, validate) {
|
|
271
|
+
if (validate && !this.endsWith(suffix))
|
|
272
272
|
throw new Error(`字符串没有以后缀 ${suffix} 结尾: ${this}`);
|
|
273
|
+
return this.slice(0, -suffix.length);
|
|
274
|
+
},
|
|
275
|
+
strip_if_end(suffix) {
|
|
276
|
+
return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this;
|
|
273
277
|
},
|
|
274
278
|
space() {
|
|
275
279
|
if (!this)
|
package/prototype.d.ts
CHANGED
|
@@ -98,10 +98,14 @@ declare global {
|
|
|
98
98
|
decode_base64(this: string, buffer: true): Buffer;
|
|
99
99
|
decode_base64(this: string, buffer?: boolean): string | Buffer;
|
|
100
100
|
space(this: string): string;
|
|
101
|
-
/**
|
|
102
|
-
strip_start(this: string, prefix: string): string;
|
|
103
|
-
/**
|
|
104
|
-
|
|
101
|
+
/** 返回去掉 prefix 开头的字符串,可选 validate = true 确保字符串以 prefix 开头(失败时抛出错误) */
|
|
102
|
+
strip_start(this: string, prefix: string, validate?: boolean): string;
|
|
103
|
+
/** 返回去掉 prefix 开头的字符串,如果没有以 prefix 开头则返回原字符串 */
|
|
104
|
+
strip_if_start(this: string, prefix: string): string;
|
|
105
|
+
/** 返回去掉 suffix 结尾的字符串,可选 validate = true 确保字符串以 suffix 结尾(失败时抛出错误) */
|
|
106
|
+
strip_end(this: string, suffix: string, validate?: boolean): string;
|
|
107
|
+
/** 返回去掉 suffix 结尾的字符串,如果没有以 suffix 结尾则返回原字符串 */
|
|
108
|
+
strip_if_end(this: string, suffix: string): string;
|
|
105
109
|
/** 等价于 .endsWith('/') */
|
|
106
110
|
isdir: boolean;
|
|
107
111
|
/** 以 `/` 分割的路径,可能以 / 结尾 */
|
package/prototype.js
CHANGED
|
@@ -278,17 +278,21 @@ if (!globalThis.my_prototype_defined) {
|
|
|
278
278
|
strip_ansi() {
|
|
279
279
|
return strip_ansi(this);
|
|
280
280
|
},
|
|
281
|
-
strip_start(prefix) {
|
|
282
|
-
if (this.startsWith(prefix))
|
|
283
|
-
return this.slice(prefix.length);
|
|
284
|
-
else
|
|
281
|
+
strip_start(prefix, validate) {
|
|
282
|
+
if (validate && !this.startsWith(prefix))
|
|
285
283
|
throw new Error(`字符串没有以前缀 ${prefix} 开头: ${this}`);
|
|
284
|
+
return this.slice(prefix.length);
|
|
286
285
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
286
|
+
strip_if_start(prefix) {
|
|
287
|
+
return this.startsWith(prefix) ? this.slice(prefix.length) : this;
|
|
288
|
+
},
|
|
289
|
+
strip_end(suffix, validate) {
|
|
290
|
+
if (validate && !this.endsWith(suffix))
|
|
291
291
|
throw new Error(`字符串没有以后缀 ${suffix} 结尾: ${this}`);
|
|
292
|
+
return this.slice(0, -suffix.length);
|
|
293
|
+
},
|
|
294
|
+
strip_if_end(suffix) {
|
|
295
|
+
return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this;
|
|
292
296
|
},
|
|
293
297
|
space() {
|
|
294
298
|
if (!this)
|
package/server.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare class Server {
|
|
|
33
33
|
name: string;
|
|
34
34
|
UAParser: typeof UAParser;
|
|
35
35
|
js_exts: Set<string>;
|
|
36
|
+
empty_body_statuses: Set<number>;
|
|
36
37
|
app: Koa;
|
|
37
38
|
handler: ReturnType<Koa['callback']>;
|
|
38
39
|
/** 启用 http server */
|
|
@@ -111,7 +112,7 @@ export declare class Server {
|
|
|
111
112
|
await this.try_send(
|
|
112
113
|
ctx,
|
|
113
114
|
`T:/t/docs${prefix_docs}`,
|
|
114
|
-
(path.endsWith('/') ? `${path}index.html` : path).
|
|
115
|
+
(path.endsWith('/') ? `${path}index.html` : path).strip_start(prefix_docs),
|
|
115
116
|
true
|
|
116
117
|
)
|
|
117
118
|
return
|
package/server.js
CHANGED
|
@@ -8,7 +8,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 { inspect, output_width, assert, range_to_numbers, encode, filter_keys, filter_values } from './utils.js';
|
|
11
|
+
import { inspect, output_width, assert, range_to_numbers, encode, filter_keys, filter_values, consume_stream } from './utils.js';
|
|
12
12
|
import { flist, fread, fstat } from './file.js';
|
|
13
13
|
// ------------ my server
|
|
14
14
|
export class Server {
|
|
@@ -24,6 +24,7 @@ export class Server {
|
|
|
24
24
|
name;
|
|
25
25
|
UAParser;
|
|
26
26
|
js_exts = new Set(['.js', '.mjs', '.cjs']);
|
|
27
|
+
empty_body_statuses = new Set([304, 204, 205]);
|
|
27
28
|
app;
|
|
28
29
|
handler;
|
|
29
30
|
/** 启用 http server */
|
|
@@ -109,13 +110,13 @@ export class Server {
|
|
|
109
110
|
// fpd_certs 文件夹下面的每个 .key 对应一个证书及域名
|
|
110
111
|
(await flist(fpd_certs, { print: false, filter: /\.key$/ }))
|
|
111
112
|
.map(async (fname) => {
|
|
112
|
-
let domain = fname.
|
|
113
|
+
let domain = fname.strip_end('.key');
|
|
113
114
|
const [key, cert] = await Promise.all([
|
|
114
115
|
fname,
|
|
115
116
|
`${domain}.crt`,
|
|
116
117
|
].map(async (fname) => fread(`${fpd_certs}${fname}`, { print: false })));
|
|
117
118
|
if (domain.startsWith('star.'))
|
|
118
|
-
domain = `*.${domain.
|
|
119
|
+
domain = `*.${domain.strip_start('star.')}`;
|
|
119
120
|
return [domain, { key, cert }];
|
|
120
121
|
})));
|
|
121
122
|
const default_ctx = lazy_secure_ctxs[this.default_hostnames.find(hostname => hostname in lazy_secure_ctxs)];
|
|
@@ -124,6 +125,7 @@ export class Server {
|
|
|
124
125
|
SNICallback(servername, callback) {
|
|
125
126
|
let lazy_ctx = lazy_secure_ctxs[servername] ||
|
|
126
127
|
lazy_secure_ctxs[servername.replace(/^.*?\./, '*.')] ||
|
|
128
|
+
lazy_secure_ctxs[`*.${servername}`] ||
|
|
127
129
|
default_ctx;
|
|
128
130
|
callback(null, lazy_ctx.ctx ??= createSecureContext(lazy_ctx));
|
|
129
131
|
},
|
|
@@ -379,9 +381,8 @@ export class Server {
|
|
|
379
381
|
// query
|
|
380
382
|
if (Object.keys(query).length) {
|
|
381
383
|
let t = inspect(query, { compact: true })
|
|
382
|
-
.replace('[Object: null prototype] ', '')
|
|
383
|
-
|
|
384
|
-
t = t.slice(0, -1);
|
|
384
|
+
.replace('[Object: null prototype] ', '')
|
|
385
|
+
.strip_if_end('\n');
|
|
385
386
|
s += (s + t).width > output_width ? '\n' : ' ';
|
|
386
387
|
s += t;
|
|
387
388
|
}
|
|
@@ -472,7 +473,17 @@ export class Server {
|
|
|
472
473
|
response.status = response_.status;
|
|
473
474
|
response.set(Server.filter_response_headers(response_.headers));
|
|
474
475
|
if (response_.body)
|
|
475
|
-
response.
|
|
476
|
+
// 如果设置了 response.status 为 304 等状态码,koa/application.js#respond() 方法中会执行
|
|
477
|
+
// ctx.body = null
|
|
478
|
+
// response.res.end()
|
|
479
|
+
// 而 res.end() 会触发 res 流的 finish, 然后由于之前 response.body = response_.body 这条语句
|
|
480
|
+
// 关联了 response_.body 到 res 的 finish 事件,(koa/response.js#set body() 中 onFinish(res, destroy.bind(null, response_.body))
|
|
481
|
+
// 会让 response_.body 这个 UndiciBodyReadable 被提前 finish,而此时
|
|
482
|
+
// 流还没 emit end, 会触发错误 RequestAbortedError [AbortError]: Request aborted
|
|
483
|
+
if (this.empty_body_statuses.has(response_.status))
|
|
484
|
+
consume_stream(response_.body, true);
|
|
485
|
+
else
|
|
486
|
+
response.body = response_.body;
|
|
476
487
|
}
|
|
477
488
|
catch (error) {
|
|
478
489
|
if (error.response?.status !== 404)
|
|
@@ -505,7 +516,7 @@ export class Server {
|
|
|
505
516
|
await this.try_send(
|
|
506
517
|
ctx,
|
|
507
518
|
`T:/t/docs${prefix_docs}`,
|
|
508
|
-
(path.endsWith('/') ? `${path}index.html` : path).
|
|
519
|
+
(path.endsWith('/') ? `${path}index.html` : path).strip_start(prefix_docs),
|
|
509
520
|
true
|
|
510
521
|
)
|
|
511
522
|
return
|
|
@@ -545,10 +556,7 @@ export class Server {
|
|
|
545
556
|
const { request, request: { method } } = ctx;
|
|
546
557
|
let { response } = ctx;
|
|
547
558
|
if (!absolute) {
|
|
548
|
-
|
|
549
|
-
fp = fp.slice(root.length);
|
|
550
|
-
if (fp.startsWith('/'))
|
|
551
|
-
fp = fp.slice(1);
|
|
559
|
+
fp = fp.strip_if_start(root).strip_if_start('/');
|
|
552
560
|
const { default: resolve_safely } = await import('resolve-path');
|
|
553
561
|
try {
|
|
554
562
|
fp = resolve_safely(root, fp).fp;
|
package/utils.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare function map_values<TValue, TNewValue>(obj: {
|
|
|
34
34
|
};
|
|
35
35
|
/** 过滤对象中的 keys, 返回新对象 */
|
|
36
36
|
export declare function filter_keys<TObj>(obj: TObj, filter: (key: string) => any): TObj;
|
|
37
|
-
/** 过滤对象中的
|
|
37
|
+
/** 过滤对象中的 values, 返回新对象 */
|
|
38
38
|
export declare function filter_values<TObj extends Record<string, any>>(obj: TObj, filter?: (value: TObj[string]) => any): TObj;
|
|
39
39
|
/** 忽略对象中的 keys, 返回新对象 */
|
|
40
40
|
export declare function omit<TObj>(obj: TObj, omit_keys: string[]): TObj;
|
|
@@ -147,7 +147,7 @@ export declare function map_stream<Out, In = Vinyl>(mapper: (obj: In, cb: Functi
|
|
|
147
147
|
failures?: boolean;
|
|
148
148
|
}): Duplex;
|
|
149
149
|
export declare function stream_to_lines(stream: Readable): AsyncGenerator<string, void, unknown>;
|
|
150
|
-
export declare function pipe_with_error(readable: Readable,
|
|
150
|
+
export declare function pipe_with_error<TWritable extends Writable>(readable: Readable, writable: TWritable): TWritable;
|
|
151
151
|
export declare class WritableMemoryStream extends Writable {
|
|
152
152
|
chunks: Buffer[];
|
|
153
153
|
pbuffer: Deferred<Buffer>;
|
|
@@ -168,6 +168,8 @@ export declare class DecoderStream extends Transform {
|
|
|
168
168
|
_transform(chunk: Uint8Array, encoding: BufferEncoding, callback: TransformCallback): void;
|
|
169
169
|
_flush(callback: TransformCallback): void;
|
|
170
170
|
}
|
|
171
|
+
/** 消费一个可读流 */
|
|
172
|
+
export declare function consume_stream(stream: Readable, ignore_error?: boolean): Promise<void>;
|
|
171
173
|
/** 根据 range 生成整数序列 (iterable)
|
|
172
174
|
- range: 取值为逗号分割的多个可用值或值区间 (不能含有空格),比如:`8321,8322,8300-8310,11000-11999`
|
|
173
175
|
- reverse?: `false` 在 range 内从后往前生成 */
|
package/utils.js
CHANGED
|
@@ -80,7 +80,7 @@ export function filter_keys(obj, filter) {
|
|
|
80
80
|
return Object.fromEntries(Object.entries(obj)
|
|
81
81
|
.filter(([key]) => filter(key)));
|
|
82
82
|
}
|
|
83
|
-
/** 过滤对象中的
|
|
83
|
+
/** 过滤对象中的 values, 返回新对象 */
|
|
84
84
|
export function filter_values(obj, filter = not_empty) {
|
|
85
85
|
return Object.fromEntries(Object.entries(obj)
|
|
86
86
|
.filter(([, value]) => filter(value)));
|
|
@@ -218,18 +218,13 @@ export async function timeout(milliseconds, action) {
|
|
|
218
218
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
219
219
|
export function defer(initial) {
|
|
220
220
|
if (initial === undefined) {
|
|
221
|
-
let resolve;
|
|
222
|
-
let reject;
|
|
223
|
-
let promise = new Promise((_resolve, _reject) => {
|
|
224
|
-
resolve = _resolve;
|
|
225
|
-
reject = _reject;
|
|
226
|
-
});
|
|
221
|
+
let { promise, resolve, reject } = Promise.withResolvers();
|
|
227
222
|
return Object.assign(promise, { resolve, reject });
|
|
228
223
|
}
|
|
229
224
|
else
|
|
230
225
|
return Object.assign(Promise.resolve(initial), {
|
|
231
|
-
resolve
|
|
232
|
-
reject
|
|
226
|
+
resolve: noop,
|
|
227
|
+
reject: noop
|
|
233
228
|
});
|
|
234
229
|
}
|
|
235
230
|
/** @example
|
|
@@ -476,10 +471,10 @@ export async function* stream_to_lines(stream) {
|
|
|
476
471
|
buf = chunk.slice(j);
|
|
477
472
|
}
|
|
478
473
|
}
|
|
479
|
-
export function pipe_with_error(readable,
|
|
474
|
+
export function pipe_with_error(readable, writable) {
|
|
480
475
|
// 不知道 transform 作为 AsyncIterable 使用时, emit error 是否会在 for await (...) 循环中触发错误
|
|
481
|
-
readable.once('error', error => {
|
|
482
|
-
return readable.pipe(
|
|
476
|
+
readable.once('error', error => { writable.emit('error', error); });
|
|
477
|
+
return readable.pipe(writable);
|
|
483
478
|
}
|
|
484
479
|
export class WritableMemoryStream extends Writable {
|
|
485
480
|
chunks = [];
|
|
@@ -525,6 +520,17 @@ export class DecoderStream extends Transform {
|
|
|
525
520
|
callback();
|
|
526
521
|
}
|
|
527
522
|
}
|
|
523
|
+
/** 消费一个可读流 */
|
|
524
|
+
export async function consume_stream(stream, ignore_error = false) {
|
|
525
|
+
try {
|
|
526
|
+
for await (const chunk of stream)
|
|
527
|
+
;
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (!ignore_error)
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
528
534
|
/** 根据 range 生成整数序列 (iterable)
|
|
529
535
|
- range: 取值为逗号分割的多个可用值或值区间 (不能含有空格),比如:`8321,8322,8300-8310,11000-11999`
|
|
530
536
|
- reverse?: `false` 在 range 内从后往前生成 */
|