xshell 1.0.59 → 1.0.61

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/.eslintrc.json CHANGED
@@ -51,6 +51,9 @@
51
51
 
52
52
  "xlint/keep-indent": "error",
53
53
 
54
+ // 函数使用 function 来声明而不是 const foo = () => { }
55
+ "xlint/func-style": "error",
56
+
54
57
 
55
58
  "@typescript-eslint/semi": ["error", "never"],
56
59
  "@typescript-eslint/no-extra-semi": "error",
@@ -170,6 +173,14 @@
170
173
 
171
174
  "@typescript-eslint/prefer-includes": "error",
172
175
 
173
- "@typescript-eslint/prefer-regexp-exec": "error"
176
+ "@typescript-eslint/prefer-regexp-exec": "error",
177
+
178
+ // 禁止使用 export default
179
+ "no-restricted-exports": ["error", { "restrictDefaultExports": { "direct": true, "named": true, "defaultFrom": true, "namedFrom": true, "namespaceFrom": true } }],
180
+
181
+ "@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports", "disallowTypeAnnotations": false }],
182
+
183
+ // () => { 返回 void 的表达式 }
184
+ "@typescript-eslint/no-confusing-void-expression": "error"
174
185
  }
175
186
  }
package/Terminal.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import 'xterm/css/xterm.css';
2
+ import { Terminal as XTermTerminal } from 'xterm';
3
+ import { Model } from 'react-object-model';
4
+ import type { Remote } from './net.browser.js';
5
+ export declare function Terminal({ font }: {
6
+ font: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ declare class TerminalModel extends Model<TerminalModel> {
9
+ term: XTermTerminal;
10
+ stdio_id: number;
11
+ subscribe_stdio(remote: Remote): Promise<void>;
12
+ unsubscribe_stdio(remote: Remote): Promise<void>;
13
+ }
14
+ export declare let terminal: TerminalModel;
15
+ export {};
package/Terminal.js ADDED
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import 'xterm/css/xterm.css';
3
+ import { useEffect, useRef } from 'react';
4
+ import { Terminal as XTermTerminal } from 'xterm';
5
+ import { FitAddon } from 'xterm-addon-fit';
6
+ import { WebglAddon } from 'xterm-addon-webgl';
7
+ import { Model } from 'react-object-model';
8
+ import { assert, genid } from './utils.browser.js';
9
+ export function Terminal({ font }) {
10
+ let rterminal = useRef();
11
+ useEffect(() => {
12
+ (async () => {
13
+ await document.fonts.ready;
14
+ let term = new XTermTerminal({
15
+ fontFamily: font,
16
+ fontSize: 16,
17
+ cursorStyle: 'bar',
18
+ disableStdin: true,
19
+ convertEol: true,
20
+ allowProposedApi: true,
21
+ theme: {
22
+ ...myscheme,
23
+ magenta: myscheme.purple,
24
+ brightMagenta: myscheme.brightPurple,
25
+ }
26
+ });
27
+ const fit_addon = new FitAddon();
28
+ term.loadAddon(fit_addon);
29
+ term.open(rterminal.current);
30
+ term.loadAddon(new WebglAddon());
31
+ fit_addon.fit();
32
+ terminal.set({ term });
33
+ })();
34
+ }, []);
35
+ return _jsx("div", { className: 'term', ref: rterminal });
36
+ }
37
+ class TerminalModel extends Model {
38
+ term;
39
+ stdio_id = 0;
40
+ async subscribe_stdio(remote) {
41
+ assert(!this.stdio_id);
42
+ const id = this.stdio_id = genid();
43
+ remote.handlers.set(id, ({ data: [chunk] }) => {
44
+ this.term.write(chunk);
45
+ });
46
+ await remote.send({ id, func: 'subscribe_stdio' });
47
+ }
48
+ async unsubscribe_stdio(remote) {
49
+ let { stdio_id } = this;
50
+ assert(stdio_id);
51
+ await remote.call('unsubscribe_stdio', [stdio_id]);
52
+ remote.handlers.delete(stdio_id);
53
+ this.stdio_id = 0;
54
+ }
55
+ }
56
+ export let terminal = new TerminalModel();
57
+ /** winterm.json */
58
+ const myscheme = {
59
+ background: '#FFFFFF',
60
+ black: '#000000',
61
+ blue: '#0070C0',
62
+ brightBlack: '#444444',
63
+ brightBlue: '#0000FF',
64
+ brightCyan: '#008080',
65
+ brightGreen: '#718C00',
66
+ brightPurple: '#8959A8',
67
+ brightRed: '#DF0000',
68
+ brightWhite: '#AAAAAA',
69
+ brightYellow: '#FF81FF',
70
+ cursorColor: '#000000',
71
+ cyan: '#821717',
72
+ foreground: '#000000',
73
+ green: '#008000',
74
+ name: 'myscheme',
75
+ purple: '#800080',
76
+ red: '#C82829',
77
+ selectionBackground: '#ADD6FF',
78
+ white: '#888888',
79
+ yellow: '#EC7600'
80
+ };
81
+ //# sourceMappingURL=Terminal.js.map
package/file.d.ts CHANGED
@@ -5,6 +5,7 @@ import { promises as fsp, default as fs } from 'fs';
5
5
  type FileHandle = fsp.FileHandle & {
6
6
  fp: string;
7
7
  };
8
+ export { fsp };
8
9
  export type Encoding = 'utf-8' | 'gb18030' | 'shift-jis' | 'utf-16le';
9
10
  export declare const encodings: readonly ["utf-8", "gb18030", "shift-jis", "utf-16le"];
10
11
  /** fp 所指向的 文件/ 文件夹 是否存在
@@ -85,8 +86,8 @@ export interface FListOptions {
85
86
  - deep?: `false` 递归遍历 recursively
86
87
  - absolute?: `false` 返回、打印完整路径而不是相对路径,为 false 时返回的 FStats.fp 也会被修改 Return, print full path instead of relative path, FStats.fp returned when false will also be modified
87
88
  - print?: `true`
88
- - filter?: `true` RegExp | (fp: string) => any 注意当 deep = true 时被 filter 过滤掉的目录及目录中的文件不会包含在结果中
89
- Note that when deep = true, directories and files in directories that are filtered out by the filter will not be included in the results
89
+ - filter?: `() => true` RegExp | (fp: string) => any
90
+ 注意当 deep = true 时被 filter 过滤掉的目录及目录中的文件不会包含在结果中
90
91
  - stats?: `false` 启用后返回 FStats 列表
91
92
  Returns the FStats list when enabled */
92
93
  export declare function flist(fpd: string): Promise<string[]>;
@@ -115,12 +116,14 @@ export declare function fdelete(fp: string, { print }?: {
115
116
  - options?:
116
117
  - print?: `true`
117
118
  - overwrite?: `true`
118
-
119
+ - filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容,和 flist 的 filter 选项一样,但只支持文件夹中第一层的文件
120
+
119
121
  @example
120
122
  fcopy('D:/temp/camera/', 'D:/camera/') */
121
- export declare function fcopy(fp_src: string, fp_dst: string, { print, overwrite, }?: {
123
+ export declare function fcopy(fp_src: string, fp_dst: string, { print, overwrite, filter, }?: {
122
124
  print?: boolean;
123
125
  overwrite?: boolean;
126
+ filter?: FListOptions['filter'];
124
127
  }): Promise<void>;
125
128
  /** 移动文件或文件夹
126
129
  相同分区 / 文件系统下使用 rename, 否则 fallback 到复制后删除源文件
@@ -177,4 +180,3 @@ export declare function ftail(fp: string, handler: (lines: string[]) => void | P
177
180
  export declare function freplace(fp: string, pattern: string | RegExp, replacement: string, { print }?: {
178
181
  print?: boolean;
179
182
  }): Promise<void>;
180
- export {};
package/file.js CHANGED
@@ -4,6 +4,7 @@ import { path } from './path.js';
4
4
  import { t } from './i18n/instance.js';
5
5
  import { to_json } from './prototype.js';
6
6
  import { assert, Lock } from './utils.js';
7
+ export { fsp };
7
8
  export const encodings = ['utf-8', 'gb18030', 'shift-jis', 'utf-16le'];
8
9
  /** fp 所指向的 文件/ 文件夹 是否存在
9
10
  Does the file/folder pointed to by fp exist? */
@@ -225,22 +226,31 @@ export async function fdelete(fp, { print = true } = {}) {
225
226
  - options?:
226
227
  - print?: `true`
227
228
  - overwrite?: `true`
228
-
229
+ - filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容,和 flist 的 filter 选项一样,但只支持文件夹中第一层的文件
230
+
229
231
  @example
230
232
  fcopy('D:/temp/camera/', 'D:/camera/') */
231
- export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, } = {}) {
233
+ export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, filter, } = {}) {
232
234
  const { isdir } = fp_src;
233
235
  assert(isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
234
236
  assert(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
237
+ if (!isdir && filter)
238
+ throw new Error(t('filter 选项只适用于 fp_src 为文件夹'));
235
239
  if (print)
236
240
  console.log(t('复制'), fp_src, '->', fp_dst);
237
241
  if (isdir)
238
- await fsp.cp(fp_src, fp_dst, {
239
- recursive: true,
240
- force: overwrite,
241
- errorOnExist: !overwrite,
242
- mode: overwrite ? 0 : fs.constants.COPYFILE_EXCL,
243
- });
242
+ if (filter) {
243
+ await fmkdir(fp_dst, { print });
244
+ await Promise.all((await flist(fp_src, { filter, print: false }))
245
+ .map(async (fname) => fcopy(fp_src + fname, fp_dst + fname, { print, overwrite })));
246
+ }
247
+ else
248
+ await fsp.cp(fp_src, fp_dst, {
249
+ recursive: true,
250
+ force: overwrite,
251
+ errorOnExist: !overwrite,
252
+ mode: overwrite ? 0 : fs.constants.COPYFILE_EXCL,
253
+ });
244
254
  else
245
255
  try {
246
256
  await fsp.copyFile(fp_src, fp_dst, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
@@ -269,14 +279,14 @@ export async function fmove(fp_src, fp_dst, { overwrite = false, print = true }
269
279
  assert(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
270
280
  if (print)
271
281
  console.log(t('移动'), fp_src, '->', fp_dst);
272
- if (!overwrite && fexists(fp_dst))
282
+ if (!overwrite && fexists(fp_dst, { print: false }))
273
283
  throw new Error(`${t('已存在')} ${fp_dst}`);
274
284
  await fmkdir(fp_dst.fdir, { print: false });
275
285
  async function copy_and_delete() {
276
- await fcopy(fp_src, fp_dst, { overwrite, print });
277
- await fdelete(fp_src, { print });
286
+ await fcopy(fp_src, fp_dst, { overwrite, print: false });
287
+ await fdelete(fp_src, { print: false });
278
288
  }
279
- if (fp_src[0] !== fp_dst[0])
289
+ if (fp_src[0] !== fp_dst[0] || overwrite)
280
290
  await copy_and_delete();
281
291
  else
282
292
  try {
@@ -305,7 +315,7 @@ export async function frename(fp, fp_, { fpd, print = true, overwrite = true } =
305
315
  assert(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
306
316
  if (print)
307
317
  console.log(t('重命名'), fp, '->', fp_);
308
- if (!overwrite && fexists(fp_))
318
+ if (!overwrite && fexists(fp_, { print: false }))
309
319
  throw new Error(`${t('已存在')} ${fp}`);
310
320
  await fsp.rename(fp, fp_);
311
321
  }
@@ -349,9 +359,9 @@ export async function flink(fp_real, fp_link, { junction = false, print = true }
349
359
  const is_fpd_link = fp_link.isdir;
350
360
  assert(is_fpd_real === is_fpd_link, t('fp_real 和 fp_link 必须同为文件路径或文件夹路径'));
351
361
  if (fexists(fp_link))
352
- throw new Error(t('存在同名') + (is_fpd_link ? t('文件夹') : t('文件')) + ': ' + fp_link + t(',无法创建链接'));
362
+ throw new Error(`${t('存在同名')}${is_fpd_link ? t('文件夹') : t('文件')}: ${fp_link}${t(',无法创建链接')}`);
353
363
  if (print)
354
- console.log(t('已将源文件 ') + fp_real + t(' 链接到 ') + fp_link);
364
+ console.log(`${t('已将源文件 ')}${fp_real}${t(' 链接到 ')}${fp_link}`);
355
365
  if (junction)
356
366
  fsp.symlink(fp_real, fp_link, 'junction');
357
367
  else
package/i18n/dict.json CHANGED
@@ -346,5 +346,8 @@
346
346
  },
347
347
  "flstat: 参数 fp: '{{fp}}' 必须是绝对路径": {
348
348
  "en": "flstat: parameter fp: '{{fp}}' must be an absolute path"
349
+ },
350
+ "filter 选项只适用于 fp_src 为文件夹": {
351
+ "en": "filter option only applies to fp_src for folders"
349
352
  }
350
353
  }
@@ -18,7 +18,7 @@ const DEFAULT_CONFIG = {
18
18
  debug: false,
19
19
  input: [
20
20
  // 'src/**/*.{js,jsx,ts,tsx}',
21
- '!i18n/**',
21
+ '!i18n/**', // Use ! to filter out files or directories
22
22
  '!node_modules/**',
23
23
  '!**/*.d.ts',
24
24
  ],
@@ -35,7 +35,7 @@ const DEFAULT_CONFIG = {
35
35
  extensions: [], // 避免在 transform 中执行原生的 parseFuncFromString
36
36
  },
37
37
  trans: {
38
- extensions: [],
38
+ extensions: [], // 避免在 transform 中执行原生的 parseTransFromString
39
39
  fallbackKey: true,
40
40
  babylon: {
41
41
  sourceType: 'module',
@@ -74,22 +74,22 @@ const DEFAULT_CONFIG = {
74
74
  }
75
75
  },
76
76
  // 禁用 : 和 . 作为 seperator
77
- keySeparator: false,
78
- nsSeparator: false,
77
+ keySeparator: false, // char to separate keys
78
+ nsSeparator: false, // char to split namespace from key
79
79
  // Context Form
80
- context: true,
81
- contextFallback: true,
82
- contextSeparator: '_',
80
+ context: true, // whether to add context form key
81
+ contextFallback: true, // whether to add a fallback key as well as the context form key
82
+ contextSeparator: '_', // char to split context from key
83
83
  // Plural
84
84
  // whether to add plural form key
85
85
  plural(language, ns, key, options /** Config */) {
86
86
  return language === 'en';
87
87
  },
88
- pluralFallback: true,
89
- pluralSeparator: '_',
88
+ pluralFallback: true, // whether to add a fallback key as well as the plural form key
89
+ pluralSeparator: '_', // char to split plural from key
90
90
  // interpolation options
91
91
  interpolation: {
92
- prefix: '{{',
92
+ prefix: '{{', // prefix for interpolation
93
93
  suffix: '}}' // suffix for interpolation
94
94
  }
95
95
  };
package/net.js CHANGED
@@ -87,7 +87,7 @@ export async function request(url, { method, queries, headers: _headers, body, t
87
87
  // --- headers, http/2 开始都用小写的 headers
88
88
  let headers = new Headers({
89
89
  'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja-JP;q=0.6,ja;q=0.5',
90
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
90
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
91
91
  'sec-ch-ua-platform': '"Windows"',
92
92
  'sec-ch-ua-platform-version': '"15.0.0"',
93
93
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.0.59",
3
+ "version": "1.0.61",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "interactive programming"
17
17
  ],
18
18
  "engines": {
19
- "node": ">=20.6.1",
19
+ "node": ">=21.3.0",
20
20
  "vscode": ">=1.81.0"
21
21
  },
22
22
  "author": "ShenHongFei <shen.hongfei@outlook.com> (https://github.com/ShenHongFei)",
@@ -45,23 +45,23 @@
45
45
  ]
46
46
  },
47
47
  "dependencies": {
48
- "@babel/core": "^7.22.17",
49
- "@babel/parser": "^7.22.16",
50
- "@babel/traverse": "^7.22.17",
48
+ "@babel/core": "^7.23.5",
49
+ "@babel/parser": "^7.23.5",
50
+ "@babel/traverse": "^7.23.5",
51
51
  "@koa/cors": "^4.0.0",
52
- "@types/ws": "^8.5.5",
52
+ "@types/ws": "^8.5.10",
53
53
  "byte-size": "^8.1.1",
54
54
  "chalk": "^5.3.0",
55
- "chardet": "^1.6.0",
55
+ "chardet": "^2.0.0",
56
56
  "cli-table3": "^0.6.3",
57
- "cli-truncate": "^3.1.0",
57
+ "cli-truncate": "^4.0.0",
58
58
  "colors": "^1.4.0",
59
- "commander": "^11.0.0",
60
- "emoji-regex": "^10.2.1",
59
+ "commander": "^11.1.0",
60
+ "emoji-regex": "^10.3.0",
61
61
  "fetch-cookie": "^2.1.0",
62
62
  "gulp-sort": "^2.0.0",
63
63
  "hash-string": "^1.0.0",
64
- "i18next": "^23.5.1",
64
+ "i18next": "^23.7.7",
65
65
  "i18next-scanner": "^4.4.0",
66
66
  "js-cookie": "^3.0.5",
67
67
  "koa": "^2.14.2",
@@ -71,42 +71,47 @@
71
71
  "mime-types": "^2.1.35",
72
72
  "ora": "^7.0.1",
73
73
  "react": "^18.2.0",
74
- "react-i18next": "^13.2.2",
74
+ "react-i18next": "^13.5.0",
75
+ "react-object-model": "^1.2.0",
75
76
  "resolve-path": "^1.4.0",
76
77
  "strip-ansi": "^7.1.0",
77
78
  "through2": "^4.0.2",
78
79
  "tough-cookie": "^4.1.3",
79
80
  "tslib": "^2.6.2",
80
- "typescript": "^5.2.2",
81
+ "typescript": "^5.3.2",
81
82
  "ua-parser-js": "2.0.0-alpha.2",
82
- "undici": "^5.24.0",
83
+ "undici": "^5.28.2",
83
84
  "vinyl": "^3.0.0",
84
85
  "vinyl-fs": "^4.0.0",
85
- "ws": "^8.14.1"
86
+ "ws": "^8.14.2",
87
+ "xterm": "^5.3.0",
88
+ "xterm-addon-fit": "^0.8.0",
89
+ "xterm-addon-web-links": "^0.9.0",
90
+ "xterm-addon-webgl": "^0.16.0"
86
91
  },
87
92
  "devDependencies": {
88
- "@babel/types": "^7.22.17",
89
- "@types/babel__traverse": "^7.20.1",
90
- "@types/byte-size": "^8.1.0",
91
- "@types/chardet": "^0.8.1",
92
- "@types/gulp-sort": "2.0.1",
93
- "@types/js-cookie": "^3.0.3",
94
- "@types/koa": "^2.13.8",
95
- "@types/koa-compress": "^4.0.3",
96
- "@types/lodash": "^4.14.198",
97
- "@types/mime-types": "^2.1.1",
98
- "@types/node": "^20.6.0",
99
- "@types/react": "^18.2.21",
100
- "@types/through2": "^2.0.39",
101
- "@types/tough-cookie": "^4.0.3",
102
- "@types/ua-parser-js": "^0.7.37",
103
- "@types/vinyl-fs": "^3.0.2",
104
- "@types/vscode": "^1.82.0",
105
- "@typescript-eslint/eslint-plugin": "^6.7.0",
106
- "@typescript-eslint/parser": "^6.7.0",
107
- "eslint": "^8.49.0",
93
+ "@babel/types": "^7.23.5",
94
+ "@types/babel__traverse": "^7.20.4",
95
+ "@types/byte-size": "^8.1.2",
96
+ "@types/chardet": "^0.8.3",
97
+ "@types/gulp-sort": "2.0.4",
98
+ "@types/js-cookie": "^3.0.6",
99
+ "@types/koa": "^2.13.12",
100
+ "@types/koa-compress": "^4.0.6",
101
+ "@types/lodash": "^4.14.202",
102
+ "@types/mime-types": "^2.1.4",
103
+ "@types/node": "^20.10.3",
104
+ "@types/react": "^18.2.41",
105
+ "@types/through2": "^2.0.41",
106
+ "@types/tough-cookie": "^4.0.5",
107
+ "@types/ua-parser-js": "^0.7.39",
108
+ "@types/vinyl-fs": "^3.0.5",
109
+ "@types/vscode": "^1.84.2",
110
+ "@typescript-eslint/eslint-plugin": "^6.13.1",
111
+ "@typescript-eslint/parser": "^6.13.1",
112
+ "eslint": "^8.55.0",
108
113
  "eslint-plugin-react": "^7.33.2",
109
- "eslint-plugin-xlint": "^1.0.8"
114
+ "eslint-plugin-xlint": "^1.0.10"
110
115
  },
111
116
  "scripts": {
112
117
  "start": "node --title=xshell --inspect=0.0.0.0:8420 ./xshell.js",
@@ -1,14 +1,14 @@
1
1
  # 用来修复 `byte_size()` 函数的 ts 类型报错
2
2
 
3
3
  diff --git a/package.json b/package.json
4
- index 4160984ea87736c65ad63d123c7d68cae96dfc1d..f9b14147ceae6c881e7f3829aca6876be317b15f 100644
4
+ index dd8ed66fcb8b1ab93f0a3a015bad95bc31d3b18d..6a3753f42610451163fd7d502a93779fe3dca8db 100644
5
5
  --- a/package.json
6
6
  +++ b/package.json
7
7
  @@ -11,6 +11,7 @@
8
- "githubUsername": "lntel"
8
+ "url": "https://github.com/lntel"
9
9
  }
10
10
  ],
11
11
  + "type": "module",
12
12
  "main": "",
13
13
  "types": "index.d.ts",
14
- "repository": {
14
+ "repository": {
package/process.d.ts CHANGED
@@ -9,8 +9,8 @@ export declare const exe_nodejs: string;
9
9
  interface StartOptions {
10
10
  /** `继承当前工作目录 process.cwd()` 子进程的工作目录 `inherit the cwd` cwd of the child process. */
11
11
  cwd?: string;
12
- /** `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env */
13
- env?: Record<string, string>;
12
+ /** `process.env` 覆盖/添加到 process.env 的环境变量 */
13
+ envs?: Record<string, string>;
14
14
  /** `'utf-8'` 子进程输出编码 child output encoding */
15
15
  encoding?: Encoding | 'binary';
16
16
  /** `true` print 选项,支持设置细项 print option (with details) */
@@ -18,6 +18,7 @@ interface StartOptions {
18
18
  stdout: boolean;
19
19
  stderr: boolean;
20
20
  command: boolean;
21
+ /** 进程执行成功的消息 */
21
22
  code: boolean;
22
23
  };
23
24
  /** `'pipe'` 设置为 'ignore' 时忽略 stdio 处理 when 'ignore' then ignore stdio processing */
@@ -41,13 +42,13 @@ interface StartOptions {
41
42
  - args: `[]` 参数列表 arguments list
42
43
  - options
43
44
  - cwd?: `继承当前工作目录 process.cwd()` 子进程的工作目录 `inherit the cwd` cwd of the child process.
44
- - env?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
45
+ - envs?: `process.env` 覆盖/添加到 process.env 的环境变量
45
46
  - encoding?: `'utf-8'` 子进程输出编码 child output encoding
46
47
  - print?: `true` print 选项,支持设置细项 print option (with details)
47
48
  - stdio?: `'pipe'` 设置为 'ignore' 时忽略 stdio 处理 when 'ignore' then ignore stdio processing
48
49
  - detached?: `false` 是否断开和 child 的关系 (ignore stdio, unref) whether to break the connection with child (ignore stdio, unref)
49
50
  */
50
- export declare function start(exe: string, args?: string[], { cwd, encoding, print, stdio, detached, env, window: _window, }?: StartOptions): Promise<ChildProcess>;
51
+ export declare function start(exe: string, args?: string[], { cwd, encoding, print, stdio, detached, envs, window: _window, }?: StartOptions): Promise<ChildProcess>;
51
52
  export declare function start_nodejs(js: string, args?: string[], options?: StartOptions): Promise<ChildProcess>;
52
53
  export interface CallOptions extends StartOptions {
53
54
  throw_code?: boolean;
@@ -67,7 +68,7 @@ export interface CallResult<TOutput extends string | Buffer = string> {
67
68
  - args: `[]` 参数列表 arguments list
68
69
  - options?:
69
70
  - cwd?: `process.cwd()`
70
- - env?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
71
+ - envs?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
71
72
  - encoding?: `'utf-8'` 子进程输出编码, 设置为 binary 时返回 stdout, stderr 类型为 Buffer; 否则是 string
72
73
  child output encoding. When set to binary, return stdout, stderr is of type Buffer; otherwise it is string
73
74
  - print?: `true` print 选项,支持设置细项 print option (with details)
@@ -80,18 +81,22 @@ export declare function call(exe: string, args?: string[], options?: CallOptions
80
81
  encoding: 'binary';
81
82
  }): Promise<CallResult<Buffer>>;
82
83
  export declare function call(exe: string, args?: string[], options?: CallOptions): Promise<CallResult<string>>;
84
+ export interface CallNodeJsOptions extends CallOptions {
85
+ inspect?: number;
86
+ break?: boolean;
87
+ }
83
88
  /** call node <js> for result
84
89
  - js: .js 路径 (相对路径根据 cwd 解析) path (relative path will resolve based on cwd)
85
90
  - args: `[]` 参数列表 arguments list
86
91
  - options
87
92
  - cwd?: `process.cwd()`
88
- - env?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
93
+ - envs?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
89
94
  - encoding?: `'utf-8'` 子进程输出编码 child process output encoding
90
95
  - print?: `true` print 选项,支持设置细项 print option (with details)
91
96
  - stdio?: `'pipe'` 设置为 'ignore' 时忽略 stdio 处理 when 'ignore' then ignore stdio processing
92
97
  - detached?: `false` 是否断开和 child 的关系 (ignore stdio, unref) whether to break the connection with child (ignore stdio, unref)
93
98
  - throw_code?: `true` code 不为 0 时是否抛出异常 whether to throw Error when code is not 0 */
94
- export declare function call_nodejs(js: string, args?: string[], options?: CallOptions): Promise<CallResult<string>>;
99
+ export declare function call_nodejs(js: string, args?: string[], { inspect, break: _break, ...options }?: CallNodeJsOptions): Promise<CallResult<string>>;
95
100
  export interface TermOptions {
96
101
  /** `process.cwd()` */
97
102
  cwd?: string;
package/process.js CHANGED
@@ -9,20 +9,20 @@ export const exe_nodejs = process.execPath.fp;
9
9
  - args: `[]` 参数列表 arguments list
10
10
  - options
11
11
  - cwd?: `继承当前工作目录 process.cwd()` 子进程的工作目录 `inherit the cwd` cwd of the child process.
12
- - env?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
12
+ - envs?: `process.env` 覆盖/添加到 process.env 的环境变量
13
13
  - encoding?: `'utf-8'` 子进程输出编码 child output encoding
14
14
  - print?: `true` print 选项,支持设置细项 print option (with details)
15
15
  - stdio?: `'pipe'` 设置为 'ignore' 时忽略 stdio 处理 when 'ignore' then ignore stdio processing
16
16
  - detached?: `false` 是否断开和 child 的关系 (ignore stdio, unref) whether to break the connection with child (ignore stdio, unref)
17
17
  */
18
- export async function start(exe, args = [], { cwd, encoding = 'utf-8', print = true, stdio = 'pipe', detached = false, env, window: _window = true, } = {}) {
18
+ export async function start(exe, args = [], { cwd, encoding = 'utf-8', print = true, stdio = 'pipe', detached = false, envs, window: _window = true, } = {}) {
19
19
  const options = {
20
20
  cwd,
21
21
  shell: false,
22
22
  windowsHide: !_window,
23
23
  stdio,
24
- ...env ? {
25
- env: { ...process.env, ...env }
24
+ ...envs ? {
25
+ env: { ...process.env, ...envs }
26
26
  } : {},
27
27
  };
28
28
  if (typeof print === 'boolean')
@@ -169,14 +169,20 @@ export async function call(exe, args = [], options = {}) {
169
169
  - args: `[]` 参数列表 arguments list
170
170
  - options
171
171
  - cwd?: `process.cwd()`
172
- - env?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
172
+ - envs?: `process.env` 覆盖/添加到 process.env 的环境变量 overwrite/add to process.env
173
173
  - encoding?: `'utf-8'` 子进程输出编码 child process output encoding
174
174
  - print?: `true` print 选项,支持设置细项 print option (with details)
175
175
  - stdio?: `'pipe'` 设置为 'ignore' 时忽略 stdio 处理 when 'ignore' then ignore stdio processing
176
176
  - detached?: `false` 是否断开和 child 的关系 (ignore stdio, unref) whether to break the connection with child (ignore stdio, unref)
177
177
  - throw_code?: `true` code 不为 0 时是否抛出异常 whether to throw Error when code is not 0 */
178
- export async function call_nodejs(js, args = [], options) {
179
- return call(exe_nodejs, [js, ...args], options);
178
+ export async function call_nodejs(js, args = [], { inspect, break: _break, ...options } = {}) {
179
+ return call(exe_nodejs, [
180
+ ...inspect ? [
181
+ _break ? `--inspect-brk=localhost:${inspect}` : `--inspect=localhost:${inspect}`
182
+ ] : [],
183
+ js,
184
+ ...args
185
+ ], options);
180
186
  }
181
187
  export const exe_winterm = `C:/Users/${userInfo().username}/AppData/Local/Microsoft/WindowsApps/wt.exe`;
182
188
  /** 新建 terminal 窗口执行进程 New Terminal window execution process
@@ -309,7 +309,7 @@ Object.defineProperties(String.prototype, {
309
309
  },
310
310
  fext() {
311
311
  const index = this.lastIndexOf('.');
312
- return index === -1 ? '' : this.slice(index + 1);
312
+ return index === -1 ? '' : this.slice(index);
313
313
  },
314
314
  }),
315
315
  });
package/server.d.ts CHANGED
@@ -63,11 +63,25 @@ export declare class Server {
63
63
  _router(ctx: Context, next: Next): Promise<void>;
64
64
  /** 被子类重写以自定义处理逻辑 */
65
65
  router(ctx: Context): Promise<boolean>;
66
+ /** 重定向 ctx 到 url (可以是完整 url 也可以是路径),设置 status code 为 code */
67
+ redirect(ctx: Context, url: string, code?: 301 | 302): void;
66
68
  logger(ctx: Context): void;
67
69
  process_ua(ctx: Context): string;
68
70
  format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
69
71
  proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>, redirect?: RequestOptions['redirect']): Promise<void>;
70
72
  static filter_response_headers(headers: Headers): {};
73
+ /** 提供静态文件
74
+ @example
75
+ const prefix_docs = '/zh/'
76
+ if (path.startsWith(prefix_docs)) {
77
+ await this.try_send(
78
+ ctx,
79
+ `T:/t/docs${prefix_docs}`,
80
+ (path.endsWith('/') ? `${path}index.html` : path).slice(prefix_docs.length),
81
+ true
82
+ )
83
+ return
84
+ } */
71
85
  try_send(ctx: Context, fpd_root: string, fp: string, log_404: boolean): Promise<boolean>;
72
86
  /** 将 body 设置为 fp 所指文件的 ReadStream
73
87
  检查 fp 是否合法,设置对应的 content-type,支持缓存,支持分块下载
package/server.js CHANGED
@@ -70,7 +70,8 @@ export class Server {
70
70
  this.server_ws = new WebSocketServer({
71
71
  noServer: true,
72
72
  skipUTF8Validation: true,
73
- perMessageDeflate: true
73
+ perMessageDeflate: true,
74
+ maxPayload: 2 ** 30 // 1 GB
74
75
  });
75
76
  this.server_ws.on('connection', (ws, request) => {
76
77
  ws.addEventListener('message', ({ data }) => {
@@ -170,10 +171,18 @@ export class Server {
170
171
  async router(ctx) {
171
172
  return false;
172
173
  }
174
+ /** 重定向 ctx 到 url (可以是完整 url 也可以是路径),设置 status code 为 code */
175
+ redirect(ctx, url, code = 301) {
176
+ const { request: { _path } } = ctx;
177
+ let { response } = ctx;
178
+ response.redirect(url);
179
+ response.status = code;
180
+ console.log(`${code} 重定向 ${_path} -> ${url}`.yellow);
181
+ }
173
182
  logger(ctx) {
174
183
  const { colors } = this;
175
184
  const { request } = ctx;
176
- const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, } = request;
185
+ const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, headers, } = request;
177
186
  let { method } = request;
178
187
  let s = '';
179
188
  // 时间
@@ -200,6 +209,16 @@ export class Server {
200
209
  return colors ? path.yellow : path;
201
210
  return path;
202
211
  })();
212
+ // range
213
+ let range = headers.range;
214
+ if (headers.range) {
215
+ let [, start, end] = /(\d*)-(\d*)/.exec(range) || [];
216
+ if (start)
217
+ start = Number(start).to_fsize_str();
218
+ if (end)
219
+ end = Number(end).to_fsize_str();
220
+ s += ` (${start} - ${end || ''})`;
221
+ }
203
222
  // query
204
223
  if (Object.keys(query).length) {
205
224
  let t = inspect(query, { compact: true })
@@ -310,6 +329,18 @@ export class Server {
310
329
  headers_[key] = value;
311
330
  return headers_;
312
331
  }
332
+ /** 提供静态文件
333
+ @example
334
+ const prefix_docs = '/zh/'
335
+ if (path.startsWith(prefix_docs)) {
336
+ await this.try_send(
337
+ ctx,
338
+ `T:/t/docs${prefix_docs}`,
339
+ (path.endsWith('/') ? `${path}index.html` : path).slice(prefix_docs.length),
340
+ true
341
+ )
342
+ return
343
+ } */
313
344
  async try_send(ctx, fpd_root, fp, log_404) {
314
345
  const { request: { _path, path, method }, response, } = ctx;
315
346
  if (response.body !== undefined || response.status !== 404)
@@ -342,7 +373,7 @@ export class Server {
342
373
  - download?: `undefined` 在 response.headers 中加上 content-disposition: attachment 指示浏览器下载文件 */
343
374
  async fsend(ctx, fp, { root, absolute, download, } = {}) {
344
375
  assert(absolute || root?.isdir, t('fsend 必须传 absolute 选项或 root 文件夹'));
345
- const { request } = ctx;
376
+ const { request, request: { method } } = ctx;
346
377
  let { response } = ctx;
347
378
  if (!absolute) {
348
379
  if (fp.startsWith(root))
@@ -372,34 +403,44 @@ export class Server {
372
403
  error.message = `fs.stat 出错: ${error.message}`;
373
404
  throw error;
374
405
  }
406
+ // 上面的 fstat 异步操作之后有可能 socket 已经关闭,那么这里什么都不做,直接结束吧
407
+ // 如果不这么做,后续 createReadStream 打开的流会赋给 response.body,但是 response.res 不再触发 finish, close 事件,
408
+ // 打开的流永远不会关闭,最终导致资源泄露,文件句柄一直打开
409
+ if (!response.socket) {
410
+ response.res.end();
411
+ return fp;
412
+ }
375
413
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
376
414
  // advertise server support of partial requests
377
415
  response.set('accept-ranges', 'bytes');
416
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
378
417
  if (!response.get('cache-control'))
379
- response.set('cache-control', 'max-age=0, must-revalidate');
418
+ response.set('cache-control', 'no-cache');
419
+ const last_modified_str = stats.mtime.toUTCString();
380
420
  if (!response.get('last-modified'))
381
- response.set('last-modified', stats.mtime.toUTCString());
421
+ response.set('last-modified', last_modified_str);
382
422
  if (download)
383
423
  response.set('content-disposition', `attachment; filename="${encodeURIComponent(fp.fname)}"`);
384
424
  if (!response.get('content-type')) {
385
425
  const { contentType: get_content_type } = await import('mime-types');
386
- const fext = fp.fext;
387
- response.set('content-type', (this.js_exts.has(fext) ? 'text/javascript; chatset=utf-8' : get_content_type(fp.fext))
426
+ const { fext } = fp;
427
+ response.set('content-type', (this.js_exts.has(fext) ? 'text/javascript; chatset=utf-8' : get_content_type(fext))
388
428
  || 'application/octet-stream');
389
429
  }
390
- if (request.fresh) {
430
+ const modified_since_str = request.get('if-modified-since');
431
+ if ((method === 'GET' || method === 'HEAD') && modified_since_str === last_modified_str) {
391
432
  response.status = 304;
392
433
  // 以上会自动设置 response.body = null
393
434
  return fp;
394
435
  }
395
- if (request.headers.range)
436
+ const range_header = request.headers.range;
437
+ if (range_header)
396
438
  try {
397
- const range_header = request.headers.range;
398
439
  const range_value = /=(.*)$/.exec(range_header)[1];
399
440
  const range = /^[\w]*?(\d*)-(\d*)$/.exec(range_value);
400
441
  assert(stats.size <= Number.MAX_SAFE_INTEGER);
401
- let start = range[1] ? parseInt(range[1]) : undefined;
402
- let end = range[2] ? parseInt(range[2]) : Number(stats.size) - 1;
442
+ let start = range[1] ? Number(range[1]) : undefined;
443
+ let end = range[2] ? Number(range[2]) : Number(stats.size) - 1;
403
444
  if (start === undefined) {
404
445
  start = Number(stats.size) - end;
405
446
  end = Number(stats.size) - 1;
@@ -52,7 +52,10 @@ export declare class Lock<TResource = void> {
52
52
  export declare function pause(milliseconds?: number): Promise<void>;
53
53
  /** 字符串字典序比较 */
54
54
  export declare function strcmp(l: string, r: string): 0 | 1 | -1;
55
+ /** 比较 1.10.02 这种版本号 */
56
+ export declare function vercmp(l: string, r: string): number;
55
57
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
58
+ export declare function global_get<TReturn = any>(keypath: string): TReturn;
56
59
  export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
57
60
  /** 拼接 TypedArrays 生成一个完整的 Uint8Array */
58
61
  export declare function concat(arrays: ArrayBufferView[]): Uint8Array;
@@ -71,3 +74,4 @@ export declare class Timer {
71
74
  getstr(): string;
72
75
  print(): void;
73
76
  }
77
+ export declare function lowercase_first_letter(str: string): string;
package/utils.browser.js CHANGED
@@ -130,12 +130,29 @@ export function strcmp(l, r) {
130
130
  return -1;
131
131
  return 1;
132
132
  }
133
+ /** 比较 1.10.02 这种版本号 */
134
+ export function vercmp(l, r) {
135
+ const lparts = l.split('.').map(x => Number(x));
136
+ const rparts = r.split('.').map(x => Number(x));
137
+ assert(lparts.length === rparts.length, '传入 vercmp 的两个版本号应该格式一致');
138
+ for (let i = 0; i < lparts.length; i++) {
139
+ const l = lparts[i];
140
+ const r = rparts[i];
141
+ assert(!isNaN(l) && !isNaN(r), '传入 vercmp 的版本非法');
142
+ if (l !== r)
143
+ return l - r;
144
+ }
145
+ return 0;
146
+ }
133
147
  export function get(obj, keypath) {
134
148
  let obj_ = obj;
135
149
  for (const key of keypath.split('.'))
136
150
  obj_ = obj_[key];
137
151
  return obj_;
138
152
  }
153
+ export function global_get(keypath) {
154
+ return get(globalThis, keypath);
155
+ }
139
156
  export function invoke(obj, funcpath, args) {
140
157
  const paths = funcpath.split('.');
141
158
  let obj_ = obj;
@@ -197,4 +214,7 @@ export class Timer {
197
214
  console.log(this.getstr());
198
215
  }
199
216
  }
217
+ export function lowercase_first_letter(str) {
218
+ return str[0].toLowerCase() + str.slice(1);
219
+ }
200
220
  //# sourceMappingURL=utils.browser.js.map
package/utils.d.ts CHANGED
@@ -23,7 +23,10 @@ export declare function unique<T>(iterable: T[] | Iterable<T>, selector?: string
23
23
  export declare function sort_keys<T>(obj: T): T;
24
24
  /** 字符串字典序比较 */
25
25
  export declare function strcmp(l: string, r: string): 0 | 1 | -1;
26
+ /** 比较 1.10.02 这种版本号 */
27
+ export declare function vercmp(l: string, r: string): number;
26
28
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
29
+ export declare function global_get<TReturn = any>(keypath: string): TReturn;
27
30
  export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
28
31
  /** 拼接 TypedArrays 生成一个完整的 Uint8Array */
29
32
  export declare function concat(arrays: ArrayBufferView[]): Uint8Array;
@@ -93,6 +96,7 @@ export declare class Lock<TResource = void> {
93
96
  }
94
97
  export declare function has_chinese(str: string): boolean;
95
98
  export declare function escape_line_feed(str: string): string;
99
+ export declare function lowercase_first_letter(str: string): string;
96
100
  /** util.inspect(obj)
97
101
  - options
98
102
  - limit?: `10000` 传 0 时不限制
package/utils.js CHANGED
@@ -90,12 +90,29 @@ export function strcmp(l, r) {
90
90
  return -1;
91
91
  return 1;
92
92
  }
93
+ /** 比较 1.10.02 这种版本号 */
94
+ export function vercmp(l, r) {
95
+ const lparts = l.split('.').map(x => Number(x));
96
+ const rparts = r.split('.').map(x => Number(x));
97
+ assert(lparts.length === rparts.length, '传入 vercmp 的两个版本号应该格式一致');
98
+ for (let i = 0; i < lparts.length; i++) {
99
+ const l = lparts[i];
100
+ const r = rparts[i];
101
+ assert(!isNaN(l) && !isNaN(r), '传入 vercmp 的版本非法');
102
+ if (l !== r)
103
+ return l - r;
104
+ }
105
+ return 0;
106
+ }
93
107
  export function get(obj, keypath) {
94
108
  let obj_ = obj;
95
109
  for (const key of keypath.split('.'))
96
110
  obj_ = obj_[key];
97
111
  return obj_;
98
112
  }
113
+ export function global_get(keypath) {
114
+ return get(globalThis, keypath);
115
+ }
99
116
  export function invoke(obj, funcpath, args) {
100
117
  const paths = funcpath.split('.');
101
118
  let obj_ = obj;
@@ -275,6 +292,9 @@ export function has_chinese(str) {
275
292
  export function escape_line_feed(str) {
276
293
  return str.replace(/\n/g, '\\n');
277
294
  }
295
+ export function lowercase_first_letter(str) {
296
+ return str[0].toLowerCase() + str.slice(1);
297
+ }
278
298
  /** util.inspect(obj)
279
299
  - options
280
300
  - limit?: `10000` 传 0 时不限制