xshell 1.0.48 → 1.0.49

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/file.js CHANGED
@@ -207,7 +207,7 @@ export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, }
207
207
  assert(fp_src.isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
208
208
  assert(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
209
209
  if (print)
210
- console.log(t('复制'), fp_src, '', fp_dst);
210
+ console.log(t('复制'), fp_src, '->', fp_dst);
211
211
  const { copy } = await import('fs-extra');
212
212
  await copy(fp_src, fp_dst, { overwrite, errorOnExist: true });
213
213
  }
@@ -222,7 +222,7 @@ export async function fmove(src, dst, { overwrite = false, print = true } = {})
222
222
  if (!path.isAbsolute(src) || !path.isAbsolute(dst))
223
223
  throw new Error(t('src 和 dst 必须为完整路径'));
224
224
  if (print)
225
- console.log(t('移动'), src, '', dst);
225
+ console.log(t('移动'), src, '->', dst);
226
226
  const { move } = await import('fs-extra');
227
227
  await move(src, dst, { overwrite });
228
228
  }
@@ -241,7 +241,7 @@ export async function frename(fp, fp_, { fpd, print = true, overwrite = true } =
241
241
  }
242
242
  assert(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
243
243
  if (print)
244
- console.log(t('重命名'), fp, '', fp_);
244
+ console.log(t('重命名'), fp, '->', fp_);
245
245
  if (!overwrite && fexists(fp_))
246
246
  throw new Error(t('文件已存在:') + fp_);
247
247
  await fsp.rename(fp, fp_);
package/i18n/dict.json CHANGED
@@ -337,5 +337,11 @@
337
337
  },
338
338
  "fsend 必须传 absolute 选项或 root 文件夹": {
339
339
  "en": "fsend must pass absolute option or root folder"
340
+ },
341
+ "message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null": {
342
+ "en": "There cannot be undefined items in the message.data array, because json will become null after serialization"
343
+ },
344
+ "xshell 启动成功,正在监听: http://localhost:8421": {
345
+ "en": "xshell started successfully and is listening: http://localhost:8421"
340
346
  }
341
347
  }
package/i18n/rwdict.js CHANGED
@@ -60,14 +60,14 @@ export class RWDict extends Dict {
60
60
  if (_translation !== translation)
61
61
  if (!overwrite) {
62
62
  console.error(`${`已存在 ${id} 词条:`.red} ${JSON.stringify(item)}`);
63
- console.error(`${'M? '.yellow}${_translation.replace(/\n/g, '\\n')} ${translation.replace(/\n/g, '\\n')}`);
63
+ console.error(`${'M? '.yellow}${_translation.replace(/\n/g, '\\n')} -> ${translation.replace(/\n/g, '\\n')}`);
64
64
  if (!dryrun)
65
65
  console.error('如要更新翻译请设置 { overwrite: true },否则使用 i18n.t(\'text\', { context: \'xxx\' }) 标记文本以区分。\n');
66
66
  return;
67
67
  }
68
68
  else {
69
69
  if (print)
70
- console.log(`${'M '.yellow}${_translation.replace(/\n/g, '\\n')} ${translation.replace(/\n/g, '\\n')}`);
70
+ console.log(`${'M '.yellow}${_translation.replace(/\n/g, '\\n')} -> ${translation.replace(/\n/g, '\\n')}`);
71
71
  if (!dryrun)
72
72
  item[language] = translation;
73
73
  }
package/net.browser.js CHANGED
@@ -380,6 +380,7 @@ export class Remote {
380
380
  作为 websocket 连接接收方,必传 websocket 参数
381
381
  发送或连接出错时自动清理 message.id 对应的 handler */
382
382
  async send(message, websocket) {
383
+ assert(!message.data || message.data.every(arg => arg !== undefined), t('message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null'));
383
384
  if (this.print)
384
385
  console.log('remote.send:', message);
385
386
  try {
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/114.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/115.0.0.0 Safari/537.36',
91
91
  });
92
92
  if (body !== undefined)
93
93
  headers.set('content-type', type);
@@ -541,6 +541,7 @@ export class Remote {
541
541
  作为 websocket 连接接收方,必传 websocket 参数
542
542
  发送或连接出错时自动清理 message.id 对应的 handler */
543
543
  async send(message, websocket) {
544
+ assert(!message.data || message.data.every(arg => arg !== undefined), 'message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null');
544
545
  if (this.print)
545
546
  console.log('remote.send:', message);
546
547
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.0.48",
3
+ "version": "1.0.49",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -67,19 +67,19 @@
67
67
  "js-cookie": "^3.0.5",
68
68
  "koa": "^2.14.2",
69
69
  "koa-compress": "^5.1.1",
70
- "koa-useragent": "^4.1.0",
71
70
  "lodash": "^4.17.21",
72
71
  "map-stream": "0.0.7",
73
72
  "mime-types": "^2.1.35",
74
73
  "ora": "^7.0.1",
75
74
  "react": "^18.2.0",
76
- "react-i18next": "^13.1.1",
75
+ "react-i18next": "^13.1.2",
77
76
  "resolve-path": "^1.4.0",
78
77
  "strip-ansi": "^7.1.0",
79
78
  "through2": "^4.0.2",
80
79
  "tough-cookie": "^4.1.3",
81
80
  "tslib": "^2.6.1",
82
81
  "typescript": "^5.1.6",
82
+ "ua-parser-js": "2.0.0-alpha.2",
83
83
  "undici": "^5.23.0",
84
84
  "vinyl": "^3.0.0",
85
85
  "vinyl-fs": "^4.0.0",
@@ -97,15 +97,16 @@
97
97
  "@types/koa-compress": "^4.0.3",
98
98
  "@types/lodash": "^4.14.197",
99
99
  "@types/mime-types": "^2.1.1",
100
- "@types/node": "^20.4.9",
100
+ "@types/node": "^20.4.10",
101
101
  "@types/react": "^18.2.20",
102
102
  "@types/through2": "^2.0.38",
103
103
  "@types/tough-cookie": "^4.0.2",
104
+ "@types/ua-parser-js": "^0.7.36",
104
105
  "@types/vinyl-fs": "^3.0.2",
105
106
  "@types/vscode": "^1.81.0",
106
107
  "@typescript-eslint/eslint-plugin": "^6.3.0",
107
108
  "@typescript-eslint/parser": "^6.3.0",
108
- "eslint": "^8.46.0",
109
+ "eslint": "^8.47.0",
109
110
  "eslint-plugin-react": "^7.33.1",
110
111
  "eslint-plugin-xlint": "^1.0.6"
111
112
  },
package/prototype.js CHANGED
@@ -93,7 +93,8 @@ if (!globalThis.my_prototype_defined) {
93
93
  cur_width += w;
94
94
  if (cur_width > width) {
95
95
  const i_fitted_next = i_fitted + 1;
96
- const t = s.slice(0, i_fitted_next) + ' '.repeat(width - 2 - fitted_width) + '…';
96
+ // winterm 中对不齐,使用 ··· 代替
97
+ const t = s.slice(0, i_fitted_next) + ' '.repeat(width - 2 - fitted_width) + '··';
97
98
  return color_bak ? color_bak + t + '\u001b[39m' : t;
98
99
  }
99
100
  }
package/repl.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { path } from './path.js';
5
5
  import { t } from './i18n/instance.js';
6
6
  import './prototype.js';
7
- import { delay, set_inspect_options, delta2str } from './utils.js';
7
+ import { delay, set_inspect_options } from './utils.js';
8
8
  import { Remote } from './net.js';
9
9
  set_inspect_options();
10
10
  let server;
@@ -12,7 +12,6 @@ let server;
12
12
  export const fpd_root = path.dirname(fileURLToPath(import.meta.url)) + '/';
13
13
  export async function start_repl() {
14
14
  // ------------ 加载库
15
- console.log(t('xshell 开始启动').yellow);
16
15
  // --- prevent from exiting
17
16
  process.on('uncaughtException', error => {
18
17
  console.error(error);
@@ -25,7 +24,6 @@ export async function start_repl() {
25
24
  useColors: true,
26
25
  terminal: true,
27
26
  });
28
- console.log(t('nodejs.repl 启动成功'));
29
27
  process.title = 'xshell';
30
28
  await Promise.all([
31
29
  pollute_global(),
@@ -44,13 +42,9 @@ export async function start_repl() {
44
42
  })
45
43
  });
46
44
  await server.start();
47
- console.log(t('server 启动成功'));
48
45
  })(),
49
46
  ]);
50
- console.log(('-'.repeat(30) + '\n' +
51
- t('xshell 启动成功,用时 ') + delta2str(new Date().getTime() - globalThis.tstarted.getTime()) + '\n' +
52
- t('正在监听: http://localhost:8421\n') +
53
- '-'.repeat(30)).green);
47
+ console.log(t('xshell 启动成功,正在监听: http://localhost:8421').green);
54
48
  }
55
49
  export async function stop() {
56
50
  console.log(t('xshell 正在退出').red);
@@ -88,7 +82,6 @@ export async function pollute_global() {
88
82
  pollute_module_exports('./server.js'),
89
83
  pollute_module_exports('./repl.js'),
90
84
  ]);
91
- console.log(t('所有模块加载成功'));
92
85
  }
93
86
  export async function pollute_module_exports(fp_mod) {
94
87
  Object.assign(globalThis, await import(fp_mod));
package/server.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="ua-parser-js" />
2
3
  /// <reference types="node" resolution-mode="require"/>
3
- import { type Server as HttpServer } from 'http';
4
+ /// <reference types="node" resolution-mode="require"/>
5
+ import { type Server as HttpServer, type IncomingHttpHeaders } from 'http';
6
+ import type { IncomingHttpHeaders as IncomingHttp2Headers } from 'http2';
4
7
  import type { WebSocketServer } from 'ws';
5
8
  import type { default as Koa, Context, Next } from 'koa';
6
- import type { UserAgentContext } from 'koa-useragent';
7
9
  declare module 'koa' {
8
10
  interface Request {
9
11
  /** 经过 decodeURIComponent 后,在路径重写之前的路径 */
@@ -12,9 +14,6 @@ declare module 'koa' {
12
14
  }
13
15
  interface Context {
14
16
  compress: boolean;
15
- userAgent: UserAgentContext['userAgent'] & {
16
- isWechat: boolean;
17
- };
18
17
  }
19
18
  }
20
19
  import { type Remote, type Headers } from './net.js';
@@ -29,6 +28,19 @@ declare module 'http' {
29
28
  export declare class Server {
30
29
  /** proxy 时需要丢弃的 resposne headers */
31
30
  static drop_response_headers: Set<string>;
31
+ UAParser: {
32
+ (uastring?: string, extensions?: Record<string, unknown>): import("ua-parser-js").IResult;
33
+ (extensions?: Record<string, unknown>): import("ua-parser-js").IResult;
34
+ new (uastring?: string, extensions?: Record<string, unknown>): import("ua-parser-js").UAParserInstance;
35
+ new (extensions?: Record<string, unknown>): import("ua-parser-js").UAParserInstance;
36
+ VERSION: string;
37
+ BROWSER: import("ua-parser-js").BROWSER;
38
+ CPU: import("ua-parser-js").CPU;
39
+ DEVICE: import("ua-parser-js").DEVICE;
40
+ ENGINE: import("ua-parser-js").ENGINE;
41
+ OS: import("ua-parser-js").OS;
42
+ UAParser: any;
43
+ };
32
44
  js_exts: Set<string>;
33
45
  app: Koa;
34
46
  handler: ReturnType<Koa['callback']>;
@@ -45,15 +57,15 @@ export declare class Server {
45
57
  start(): Promise<void>;
46
58
  stop(): void;
47
59
  entry(ctx: Context, next: Next): Promise<void>;
48
- /**
49
- parse req.body to request.body
50
- process request.ip
51
- */
60
+ /** 解析 req.body to request.body
61
+ 处理 request.ip */
52
62
  parse(ctx: Context): Promise<void>;
53
63
  _router(ctx: Context, next: Next): Promise<void>;
54
64
  /** 被子类重写以自定义处理逻辑 */
55
65
  router(ctx: Context): Promise<boolean>;
56
66
  logger(ctx: Context): void;
67
+ process_ua(ctx: Context): string;
68
+ format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
57
69
  proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>): Promise<void>;
58
70
  static filter_response_headers(headers: Headers): {};
59
71
  try_send(ctx: Context, fp: string, { root, log_404, }: {
package/server.js CHANGED
@@ -3,7 +3,6 @@ import zlib from 'zlib';
3
3
  import { createReadStream } from 'fs';
4
4
  import { Readable } from 'stream';
5
5
  import util from 'util';
6
- import querystring from 'querystring';
7
6
  // --- my libs
8
7
  import { t } from './i18n/instance.js';
9
8
  import { request as _request } from './net.js';
@@ -20,6 +19,7 @@ export class Server {
20
19
  'transfer-encoding',
21
20
  'x-powered-by'
22
21
  ]);
22
+ UAParser = null;
23
23
  js_exts = new Set(['.js', '.mjs', '.cjs']);
24
24
  app;
25
25
  handler;
@@ -39,7 +39,8 @@ export class Server {
39
39
  const { default: Koa } = await import('koa');
40
40
  const { default: KoaCors } = await import('@koa/cors');
41
41
  const { default: KoaCompress } = await import('koa-compress');
42
- const { userAgent: KoaUserAgent } = await import('koa-useragent');
42
+ const { UAParser } = await import('ua-parser-js');
43
+ this.UAParser = UAParser;
43
44
  const { WebSocketServer } = await import('ws');
44
45
  // --- init koa app
45
46
  let app = new Koa();
@@ -60,7 +61,6 @@ export class Server {
60
61
  threshold: 512
61
62
  }));
62
63
  app.use(KoaCors({ credentials: true }));
63
- app.use(KoaUserAgent);
64
64
  app.use(this._router.bind(this));
65
65
  this.app = app;
66
66
  this.handler = this.app.callback();
@@ -79,9 +79,23 @@ export class Server {
79
79
  });
80
80
  this.server_http.on('upgrade', (request, socket, head) => {
81
81
  // url 只有路径部分
82
- const { url, headers: { host = '', 'user-agent': ua }, } = request;
82
+ const { url, headers, headers: { host = '' }, } = request;
83
83
  const ip = request.socket.remoteAddress.replace(/^::ffff:/, '');
84
- console.log(`${new Date().to_time_str()} ${(ip || '').pad(40)} ${(ua || '').limit(40)} ${'websocket'.pad(10).magenta} ${'connect'.pad(10).magenta}${host.pad(20)} ${url.pad(60).yellow}`);
84
+ console.log(
85
+ // 时间
86
+ `${new Date().to_time_str()} ` +
87
+ // ip(位置)
88
+ (ip || '').limit(60) + ' ' +
89
+ // ua
90
+ this.format_ua(headers).limit(56) + ' ' +
91
+ // https/2.0
92
+ `${this.colors ? 'websocket'.limit(10).magenta : 'websocket'.limit(10)} ` +
93
+ // method
94
+ ''.limit(6) + ' ' +
95
+ // host
96
+ `${host.limit(24)} ` +
97
+ // path
98
+ (this.colors ? url.yellow : url));
85
99
  switch (url) {
86
100
  case '/':
87
101
  this.server_ws.handleUpgrade(request, socket, head, ws => {
@@ -117,10 +131,8 @@ export class Server {
117
131
  response.type = 'text/plain';
118
132
  }
119
133
  }
120
- /**
121
- parse req.body to request.body
122
- process request.ip
123
- */
134
+ /** 解析 req.body to request.body
135
+ 处理 request.ip */
124
136
  async parse(ctx) {
125
137
  let { req, request } = ctx;
126
138
  const buf = await stream_to_buffer(req);
@@ -131,14 +143,10 @@ export class Server {
131
143
  // --- parse body
132
144
  if (!req.body)
133
145
  return;
134
- if (ctx.is('application/json') || ctx.is('text/plain'))
135
- request.body = JSON.parse(req.body.toString());
136
- else if (ctx.is('application/x-www-form-urlencoded'))
137
- request.body = querystring.parse(req.body.toString());
138
- else if (ctx.is('multipart/form-data'))
139
- throw new Error('multipart/form-data is not supported');
140
- else
141
- request.body = req.body;
146
+ request.body = ctx.is('application/json') || ctx.is('text/plain') ?
147
+ JSON.parse(req.body.toString())
148
+ :
149
+ req.body;
142
150
  }
143
151
  async _router(ctx, next) {
144
152
  let { request } = ctx;
@@ -167,49 +175,32 @@ export class Server {
167
175
  const { request } = ctx;
168
176
  const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, } = request;
169
177
  let { method } = request;
170
- const ua = ctx.userAgent;
171
178
  let s = '';
172
- // --- time
173
- s += `${new Date().to_time_str()} `;
174
- // --- ip
175
- s += (ip || '').pad(40) + ' ';
176
- // --- ua
177
- s += (() => {
178
- let t = '';
179
- if (ua.isMobile)
180
- t += 'mobile';
181
- if (ua.isDesktop)
182
- t += 'desktop';
183
- if (ua.isBot)
184
- t += `${t ? ' ' : ''}${colors ? 'robot'.blue : 'robot'}`;
185
- if (ua.platform !== 'unknown' && !ua.os.startsWith('Windows'))
186
- t += '/' + ua.platform.toLowerCase().replace('apple mac', 'mac');
187
- if (ua.os !== 'unknown' && ua.platform !== 'Android')
188
- t += '/' + ua.os.toLowerCase();
189
- if (ua.browser !== 'unknown')
190
- t += '/' + ua.browser.toLowerCase();
191
- if (ua.isWechat)
192
- t += '/weixin';
193
- if (ua.version !== 'unknown')
194
- t += '/' + ua.version.split('.').slice(0, 2).join('.');
195
- return t;
196
- })().pad(40) + ' ';
197
- // --- https/2.0
198
- s += `${`${protocol.pad(5)}/${http_version}`.pad(10)} `;
199
- // --- method
179
+ // 时间
180
+ s += `${new Date().to_time_str()} `;
181
+ // ip(位置)
182
+ s += (ip || '').limit(60) + ' ';
183
+ // ua
184
+ s += this.process_ua(ctx).limit(56) + ' ';
185
+ // https/2.0
186
+ s += `${`${protocol.limit(5)} ${http_version}`.limit(10)} `;
187
+ // method
200
188
  method = method.toLowerCase();
201
- s += (method === 'get' || !colors) ? method.pad(10) : method.pad(10).yellow;
202
- // --- host
203
- s += `${host.pad(20)} `;
204
- // --- path
189
+ let t = method.limit(6) + ' ';
190
+ if (colors && method !== 'get')
191
+ method = t.yellow;
192
+ s += t;
193
+ // host
194
+ s += `${host.limit(24)} `;
195
+ // path
205
196
  s += (() => {
206
197
  if (path.toLowerCase() !== _path.toLowerCase())
207
- return `${_path.blue} ${path}`;
198
+ return `${_path.blue} -> ${path}`;
208
199
  if (!path.includes('.'))
209
200
  return colors ? path.yellow : path;
210
201
  return path;
211
202
  })();
212
- // --- query
203
+ // query
213
204
  if (Object.keys(query).length) {
214
205
  let t = inspect(query, { compact: true })
215
206
  .replace('[Object: null prototype] ', '');
@@ -218,12 +209,59 @@ export class Server {
218
209
  s += (s + t).width > output_width ? '\n' : ' ';
219
210
  s += t;
220
211
  }
221
- // --- body
212
+ // body
222
213
  if (body && Object.keys(body).length)
223
214
  s += '\n' + inspect(body).replace('[Object: null prototype] ', '');
224
- // --- print log
215
+ // 打印日志
225
216
  console.log(s);
226
217
  }
218
+ process_ua(ctx) {
219
+ const { headers, headers: { 'user-agent': ua_str } } = ctx.request;
220
+ if (!ua_str)
221
+ return '';
222
+ ctx.response.set('accept-ch', 'Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, RTT, Device-Memory');
223
+ return this.format_ua(headers);
224
+ }
225
+ format_ua(headers) {
226
+ const { rtt, 'device-memory': memory } = headers;
227
+ const { device, os, browser } = this.UAParser(headers).withClientHints();
228
+ const vendor = device.vendor?.toLowerCase();
229
+ const model = device.model?.toLowerCase();
230
+ const osname = os.name?.toLowerCase();
231
+ const browser_name = browser.name?.toLowerCase();
232
+ let s = '';
233
+ if (vendor && vendor !== 'apple')
234
+ s += vendor;
235
+ if (model && model !== 'k' && model !== 'macintosh') {
236
+ if (s)
237
+ s += ' ';
238
+ s += model.replace('22127rk46c', 'redmi k60 pro');
239
+ }
240
+ if (osname) {
241
+ if (s)
242
+ s += '/';
243
+ s += osname;
244
+ if (os.version)
245
+ s += ` ${os.version.split('.')[0].toLowerCase()}`;
246
+ }
247
+ if (browser_name) {
248
+ if (s)
249
+ s += '/';
250
+ s += browser_name.replace('mobile chrome', 'chrome');
251
+ if (browser.version) {
252
+ const [major, minor] = browser.version.toLowerCase().split('.');
253
+ s += ` ${major}`;
254
+ if (minor && minor !== '0')
255
+ s += `.${minor}`;
256
+ }
257
+ }
258
+ if (memory) {
259
+ s += `/${memory}g`;
260
+ if (rtt && rtt !== '0')
261
+ s += ` ${rtt}ms`;
262
+ }
263
+ return s;
264
+ }
227
265
  async proxy(ctx, url, headers_ = {}) {
228
266
  const { request: { method, headers, query, body } } = ctx;
229
267
  let { response } = ctx;
@@ -277,7 +315,7 @@ export class Server {
277
315
  if (method !== 'HEAD' && method !== 'GET')
278
316
  return false;
279
317
  function _log_404() {
280
- let s = `${' '.repeat(13)} ${method.toLowerCase()} 404: ${path}`;
318
+ let s = `${' '.repeat(11)} ${method.toLowerCase()} 404: ${path}`;
281
319
  if (_path !== path)
282
320
  s += ` ${_path.bracket()}`;
283
321
  console.log(s.red);
@@ -1,5 +1,8 @@
1
1
  export declare const noop: () => void;
2
2
  export declare function assert(assertion: any, message?: string): never | void;
3
+ /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
4
+ @example seq(10, i => `item-${i}`) */
5
+ export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
3
6
  export declare function delay(milliseconds: number): Promise<void>;
4
7
  export declare function timeout(milliseconds: number): Promise<void>;
5
8
  /** https://stackoverflow.com/questions/63297164/how-to-only-accept-arraybuffer-as-parameter */
package/utils.browser.js CHANGED
@@ -6,6 +6,14 @@ export function assert(assertion, message) {
6
6
  throw Object.assign(new Error(`断言失败: ${message ? `${message}: ` : ''}${assertion}`), { assertion });
7
7
  }
8
8
  }
9
+ /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
10
+ @example seq(10, i => `item-${i}`) */
11
+ export function seq(n, generator) {
12
+ let a = new Array(n);
13
+ for (let i = 0; i < n; i++)
14
+ a[i] = generator ? generator(i) : i;
15
+ return a;
16
+ }
9
17
  export async function delay(milliseconds) {
10
18
  return new Promise(resolve => {
11
19
  setTimeout(resolve, milliseconds);
package/utils.d.ts CHANGED
@@ -10,6 +10,9 @@ export declare const noop: () => void;
10
10
  export declare const output_width = 230;
11
11
  export declare function set_inspect_options(colors?: boolean): void;
12
12
  export declare function assert(assertion: any, message?: string): never | void;
13
+ /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
14
+ @example seq(10, i => `item-${i}`) */
15
+ export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
13
16
  export declare function dedent(templ: TemplateStringsArray | string, ...values: any[]): string;
14
17
  /** 数组或 iterable 去重(可按 selector 去重)
15
18
  - selector?: 可以是 key (string) 或 (obj: any) => any
package/utils.js CHANGED
@@ -27,6 +27,14 @@ export function assert(assertion, message) {
27
27
  throw Object.assign(new Error(`断言失败: ${message ? `${message}: ` : ''}${inspect(assertion, { colors: false, compact: true })}`), { assertion });
28
28
  }
29
29
  }
30
+ /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
31
+ @example seq(10, i => `item-${i}`) */
32
+ export function seq(n, generator) {
33
+ let a = new Array(n);
34
+ for (let i = 0; i < n; i++)
35
+ a[i] = generator ? generator(i) : i;
36
+ return a;
37
+ }
30
38
  export function dedent(templ, ...values) {
31
39
  let strings = Array.from(typeof templ === 'string' ? [templ] : templ.raw);
32
40
  // 1. remove trailing whitespace