xshell 1.0.60 → 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/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
@@ -116,12 +116,14 @@ export declare function fdelete(fp: string, { print }?: {
116
116
  - options?:
117
117
  - print?: `true`
118
118
  - overwrite?: `true`
119
-
119
+ - filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容,和 flist 的 filter 选项一样,但只支持文件夹中第一层的文件
120
+
120
121
  @example
121
122
  fcopy('D:/temp/camera/', 'D:/camera/') */
122
- 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, }?: {
123
124
  print?: boolean;
124
125
  overwrite?: boolean;
126
+ filter?: FListOptions['filter'];
125
127
  }): Promise<void>;
126
128
  /** 移动文件或文件夹
127
129
  相同分区 / 文件系统下使用 rename, 否则 fallback 到复制后删除源文件
package/file.js CHANGED
@@ -226,22 +226,31 @@ export async function fdelete(fp, { print = true } = {}) {
226
226
  - options?:
227
227
  - print?: `true`
228
228
  - overwrite?: `true`
229
-
229
+ - filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容,和 flist 的 filter 选项一样,但只支持文件夹中第一层的文件
230
+
230
231
  @example
231
232
  fcopy('D:/temp/camera/', 'D:/camera/') */
232
- 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, } = {}) {
233
234
  const { isdir } = fp_src;
234
235
  assert(isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
235
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 为文件夹'));
236
239
  if (print)
237
240
  console.log(t('复制'), fp_src, '->', fp_dst);
238
241
  if (isdir)
239
- await fsp.cp(fp_src, fp_dst, {
240
- recursive: true,
241
- force: overwrite,
242
- errorOnExist: !overwrite,
243
- mode: overwrite ? 0 : fs.constants.COPYFILE_EXCL,
244
- });
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
+ });
245
254
  else
246
255
  try {
247
256
  await fsp.copyFile(fp_src, fp_dst, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
@@ -306,7 +315,7 @@ export async function frename(fp, fp_, { fpd, print = true, overwrite = true } =
306
315
  assert(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
307
316
  if (print)
308
317
  console.log(t('重命名'), fp, '->', fp_);
309
- if (!overwrite && fexists(fp_))
318
+ if (!overwrite && fexists(fp_, { print: false }))
310
319
  throw new Error(`${t('已存在')} ${fp}`);
311
320
  await fsp.rename(fp, fp_);
312
321
  }
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.60",
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,11 +45,11 @@
45
45
  ]
46
46
  },
47
47
  "dependencies": {
48
- "@babel/core": "^7.23.3",
49
- "@babel/parser": "^7.23.3",
50
- "@babel/traverse": "^7.23.3",
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.9",
52
+ "@types/ws": "^8.5.10",
53
53
  "byte-size": "^8.1.1",
54
54
  "chalk": "^5.3.0",
55
55
  "chardet": "^2.0.0",
@@ -61,7 +61,7 @@
61
61
  "fetch-cookie": "^2.1.0",
62
62
  "gulp-sort": "^2.0.0",
63
63
  "hash-string": "^1.0.0",
64
- "i18next": "^23.6.0",
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,40 +71,45 @@
71
71
  "mime-types": "^2.1.35",
72
72
  "ora": "^7.0.1",
73
73
  "react": "^18.2.0",
74
- "react-i18next": "^13.3.1",
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.27.2",
83
+ "undici": "^5.28.2",
83
84
  "vinyl": "^3.0.0",
84
85
  "vinyl-fs": "^4.0.0",
85
- "ws": "^8.14.2"
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.23.3",
93
+ "@babel/types": "^7.23.5",
89
94
  "@types/babel__traverse": "^7.20.4",
90
95
  "@types/byte-size": "^8.1.2",
91
96
  "@types/chardet": "^0.8.3",
92
97
  "@types/gulp-sort": "2.0.4",
93
98
  "@types/js-cookie": "^3.0.6",
94
- "@types/koa": "^2.13.11",
99
+ "@types/koa": "^2.13.12",
95
100
  "@types/koa-compress": "^4.0.6",
96
- "@types/lodash": "^4.14.201",
101
+ "@types/lodash": "^4.14.202",
97
102
  "@types/mime-types": "^2.1.4",
98
- "@types/node": "^20.9.0",
99
- "@types/react": "^18.2.37",
103
+ "@types/node": "^20.10.3",
104
+ "@types/react": "^18.2.41",
100
105
  "@types/through2": "^2.0.41",
101
106
  "@types/tough-cookie": "^4.0.5",
102
107
  "@types/ua-parser-js": "^0.7.39",
103
108
  "@types/vinyl-fs": "^3.0.5",
104
- "@types/vscode": "^1.84.1",
105
- "@typescript-eslint/eslint-plugin": "^6.10.0",
106
- "@typescript-eslint/parser": "^6.10.0",
107
- "eslint": "^8.53.0",
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
114
  "eslint-plugin-xlint": "^1.0.10"
110
115
  },
package/path.d.ts CHANGED
@@ -54,7 +54,7 @@ export declare function extname(path: string): string;
54
54
  /** `/` */
55
55
  export declare const sep = "/";
56
56
  /** The platform-specific file delimiter. ';' or ':'. */
57
- export declare const delimiter: ";" | ":";
57
+ export declare const delimiter: ":" | ";";
58
58
  /** Returns an object from a path string - the opposite of format().
59
59
  @param path path to evaluate.
60
60
  @throws {TypeError} if `path` is not a string. */
@@ -81,7 +81,7 @@ export declare let path: {
81
81
  basename: typeof basename;
82
82
  extname: typeof extname;
83
83
  sep: string;
84
- delimiter: ";" | ":";
84
+ delimiter: ":" | ";";
85
85
  parse: typeof parse;
86
86
  format: typeof format;
87
87
  toNamespacedPath: typeof toNamespacedPath;
package/process.d.ts CHANGED
@@ -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 */
@@ -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,6 +63,8 @@ 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;
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 })
@@ -354,7 +373,7 @@ export class Server {
354
373
  - download?: `undefined` 在 response.headers 中加上 content-disposition: attachment 指示浏览器下载文件 */
355
374
  async fsend(ctx, fp, { root, absolute, download, } = {}) {
356
375
  assert(absolute || root?.isdir, t('fsend 必须传 absolute 选项或 root 文件夹'));
357
- const { request } = ctx;
376
+ const { request, request: { method } } = ctx;
358
377
  let { response } = ctx;
359
378
  if (!absolute) {
360
379
  if (fp.startsWith(root))
@@ -384,34 +403,44 @@ export class Server {
384
403
  error.message = `fs.stat 出错: ${error.message}`;
385
404
  throw error;
386
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
+ }
387
413
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
388
414
  // advertise server support of partial requests
389
415
  response.set('accept-ranges', 'bytes');
416
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
390
417
  if (!response.get('cache-control'))
391
- response.set('cache-control', 'max-age=0, must-revalidate');
418
+ response.set('cache-control', 'no-cache');
419
+ const last_modified_str = stats.mtime.toUTCString();
392
420
  if (!response.get('last-modified'))
393
- response.set('last-modified', stats.mtime.toUTCString());
421
+ response.set('last-modified', last_modified_str);
394
422
  if (download)
395
423
  response.set('content-disposition', `attachment; filename="${encodeURIComponent(fp.fname)}"`);
396
424
  if (!response.get('content-type')) {
397
425
  const { contentType: get_content_type } = await import('mime-types');
398
- const fext = fp.fext;
399
- 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))
400
428
  || 'application/octet-stream');
401
429
  }
402
- 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) {
403
432
  response.status = 304;
404
433
  // 以上会自动设置 response.body = null
405
434
  return fp;
406
435
  }
407
- if (request.headers.range)
436
+ const range_header = request.headers.range;
437
+ if (range_header)
408
438
  try {
409
- const range_header = request.headers.range;
410
439
  const range_value = /=(.*)$/.exec(range_header)[1];
411
440
  const range = /^[\w]*?(\d*)-(\d*)$/.exec(range_value);
412
441
  assert(stats.size <= Number.MAX_SAFE_INTEGER);
413
- let start = range[1] ? parseInt(range[1]) : undefined;
414
- 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;
415
444
  if (start === undefined) {
416
445
  start = Number(stats.size) - end;
417
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;
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;
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;
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;