xshell 1.2.47 → 1.2.48

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/builder.js CHANGED
@@ -316,6 +316,9 @@ export class Bundler {
316
316
  transpileOnly: !this.dts,
317
317
  compilerOptions: {
318
318
  module: 'ESNext',
319
+ // 编译 using 和 await using
320
+ // todo: 等 webpack 的 acorn 原生支持解析语法后可删去
321
+ target: 'ES2024',
319
322
  moduleResolution: 'Bundler',
320
323
  declaration: this.dts,
321
324
  noEmit: false,
package/i18n/i18n-scan.js CHANGED
@@ -2,19 +2,10 @@
2
2
  import { program } from 'commander';
3
3
  import { path } from "../path.js";
4
4
  import { scanner } from "./scanner/index.js";
5
- import { try_load_dict } from "./utils.js";
6
- (async function main() {
7
- program.name('i18n-scan')
8
- .option('-r, --rootdir [rootdir]', '根目录:默认为当前工作目录', path.normalize(process.cwd()))
9
- .option('-i, --input [input]', '扫描 pattern:多个 pattern 用分号分割,采用 glob pattern 匹配,如 `src/**/*.{js,jsx,ts,tsx}`', v => v.split(';'))
10
- .option('-o, --output [output]', 'i18n 目录:默认为 <rootdir>/i18n/')
11
- .option('-c, --config [config]', '自定义配置文件,默认为 <rootdir>/i18n/config.js ,可参考默认配置 xshell/i18n/index.ts 以及 https://github.com/i18next/i18next-scanner', 'i18n/config.js')
12
- .parse(process.argv);
13
- const { rootdir, config, input, output } = program.opts();
14
- scanner(rootdir, {
15
- ...await try_load_dict(path.resolve(rootdir, config)),
16
- ...input ? { input } : {},
17
- ...output ? { output } : {},
18
- });
19
- })();
5
+ program.name('i18n-scan')
6
+ .option('-r, --rootdir [rootdir]', '根目录:默认为当前工作目录', path.normalize(process.cwd()).fpd)
7
+ .option('-o, --output [output]', 'i18n 目录:默认为 <rootdir>/i18n/')
8
+ .parse(process.argv);
9
+ const { rootdir, output } = program.opts();
10
+ await scanner(rootdir, output ? { output } : undefined);
20
11
  //# sourceMappingURL=i18n-scan.js.map
package/i18n/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type i18n as I18Next } from 'i18next';
2
2
  import type { Trans } from 'react-i18next';
3
3
  import { type _Dict, type Item } from './dict.ts';
4
4
  export type Language = 'zh' | 'en' | 'ja' | 'ko';
5
- export declare const LANGUAGES: readonly ["zh", "en", "ja", "ko"];
5
+ export declare const languages: readonly ["zh", "en", "ja", "ko"];
6
6
  declare global {
7
7
  interface Window {
8
8
  language: Language;
package/i18n/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { default as i18next } from 'i18next';
2
2
  import { Dict } from "./dict.js";
3
- export const LANGUAGES = ['zh', 'en', 'ja', 'ko'];
3
+ export const languages = ['zh', 'en', 'ja', 'ko'];
4
4
  /**
5
5
  提供翻译文本功能,自动解析当前语言
6
6
  @see https://github.com/ShenHongFei/xshell/tree/master/i18n
@@ -1,8 +1,12 @@
1
1
  import '../../prototype.ts';
2
+ /** 扫描源码中的词条,以及收集未翻译的词条,将结果保存到 dict.json 和 untranslateds.json
3
+ - `process.cwd()` rootdir 要扫描根目录
4
+ - config 配置信息 */
5
+ export declare function scanner(fpd_root: string, config?: Config): Promise<number>;
2
6
  /** 默认 i18next 扫描配置 */
3
- declare const DEFAULT_CONFIG: {
7
+ declare const default_config: {
4
8
  debug: boolean;
5
- input: string[];
9
+ input: any[];
6
10
  output: string;
7
11
  dict: string[];
8
12
  lngs: string[];
@@ -35,7 +39,7 @@ declare const DEFAULT_CONFIG: {
35
39
  suffix: string;
36
40
  };
37
41
  };
38
- export type Config = Partial<(typeof DEFAULT_CONFIG) & {
42
+ export type Config = Partial<(typeof default_config) & {
39
43
  defaultValue?: string;
40
44
  resource?: {
41
45
  loadPath?: string;
@@ -44,9 +48,4 @@ export type Config = Partial<(typeof DEFAULT_CONFIG) & {
44
48
  lineEnding?: '\n';
45
49
  };
46
50
  }>;
47
- /** 扫描源码中的词条,以及收集未翻译的词条,将结果保存到 dict.json 和 untranslateds.json
48
- - `process.cwd()` rootdir 要扫描根目录
49
- - config 配置信息
50
- */
51
- export declare function scanner(rootdir?: string, config?: Config): Promise<number>;
52
51
  export {};
@@ -1,27 +1,155 @@
1
- import i18n_scanner from 'i18next-scanner';
2
- import vfs from 'vinyl-fs';
3
- import sort from 'gulp-sort';
4
- import ora from 'ora';
5
- import cli_truncate from 'cli-truncate';
6
- import Vinyl from 'vinyl';
7
- import through2 from 'through2';
8
- import CliTable from 'cli-table3';
1
+ import { Parser } from 'i18next-scanner';
9
2
  import "../../prototype.js";
10
3
  import { path } from "../../path.js";
11
- import { map_stream } from "../../utils.js";
12
- import { LANGUAGES } from "../index.js";
4
+ import { flist, fread, fwrite } from "../../file.js";
5
+ import { noprint } from "../../process.js";
6
+ import { rethrow } from "../../prototype.js";
7
+ import { languages } from "../index.js";
13
8
  import { RWDict } from "../rwdict.js";
14
9
  import { try_load_dict } from "../utils.js";
15
- import { mix_parse_trans_from_string_by_babel } from "./parser.js";
10
+ import { parse_trans_from_string_by_babel } from "./parser.js";
11
+ /** 扫描源码中的词条,以及收集未翻译的词条,将结果保存到 dict.json 和 untranslateds.json
12
+ - `process.cwd()` rootdir 要扫描根目录
13
+ - config 配置信息 */
14
+ export async function scanner(fpd_root, config = {}) {
15
+ const fpd_out = path.resolve(fpd_root, config.output || default_config.output).fpd;
16
+ config = {
17
+ ...default_config,
18
+ ...config,
19
+ output: fpd_out,
20
+ resource: {
21
+ loadPath: '',
22
+ savePath: `${fpd_out}translation/{{lng}}.js`,
23
+ jsonIndent: 4,
24
+ lineEnding: '\n'
25
+ }
26
+ };
27
+ let dict = new RWDict();
28
+ for (const fp_dict of config.dict)
29
+ dict.merge(await try_load_dict(`${fpd_out}${fp_dict}`), { print: false, overwrite: true });
30
+ // 所有语言的扫描统计信息
31
+ let stats = {};
32
+ for (const language of languages)
33
+ stats[language] = {
34
+ translateds: new Set(),
35
+ untranslateds: new Set()
36
+ };
37
+ function on_scanned(text, { language, key, defaultValue, count, context }) {
38
+ // console.log(text, { language, key, defaultValue, count, context })
39
+ text ||= defaultValue;
40
+ if (!key)
41
+ key = context ? `${text}_${context}` : text;
42
+ if (!language) {
43
+ for (const language of languages)
44
+ on_scanned(text, { language, key, count, context });
45
+ return;
46
+ }
47
+ // console.log(text, { language, key, defaultValue, count, context })
48
+ const stat = stats[language];
49
+ // 获取已有翻译
50
+ const translation = dict.get(key, language) ||
51
+ language === 'zh' && text ||
52
+ '';
53
+ if (language === 'zh' && !context)
54
+ return;
55
+ if (translation)
56
+ stat.translateds.add(key);
57
+ else
58
+ stat.untranslateds.add(key);
59
+ if (language === 'en' && count !== undefined)
60
+ on_scanned(text, { language, key: `${key}_plural`, context });
61
+ }
62
+ let parser = new Parser(config);
63
+ let mixed = false;
64
+ (await Promise.all((await flist(fpd_root, {
65
+ print: false,
66
+ deep: true,
67
+ filter: fp => fp.isdir ?
68
+ !(fp.startsWith('.') || excludes.includes(fp.fname))
69
+ :
70
+ fp.endsWith('.ts') && !fp.endsWith('.d.ts') || fp.endsWith('.tsx')
71
+ }))
72
+ .filter(fp => !fp.isdir)
73
+ .map(async (fp) => [fp, await fread(`${fpd_root}${fp}`, noprint)]))).forEach(([fp, code]) => {
74
+ // --- 添加代码中扫描到的 t('key') 中的 key 到 parser
75
+ // parser.parseFuncFromString 使用 esprima 来解析代码,esprima 仍然不支持 optional chaining !!
76
+ parser.parseFuncFromString(code.replace(/\?\.\[/g, '[').replace(/\?\.\(/g, '(').replace(/\?\./g, '.'), on_scanned);
77
+ // --- 添加代码中扫描到的 Trans 组件中的 key 到 parser
78
+ if (fp.endsWith('.tsx') && code.includes('<Trans')) {
79
+ // parser.parseTransFromString 使用 acorn 解析代码,不支持 TypeScript,添加 parser.parseTransFromStringByBabel
80
+ if (!mixed) {
81
+ mixed = true;
82
+ parser.parseTransFromStringByBabel = parse_trans_from_string_by_babel;
83
+ }
84
+ parser.parseTransFromStringByBabel(code, { filepath: fp }, on_scanned, rethrow);
85
+ }
86
+ });
87
+ // --- 打印词条统计表
88
+ console.log('扫描完成\n' +
89
+ pad('语言', 0) + pad('未翻译', 1).red + pad('已翻译', 2).green + '\n' +
90
+ Object.entries(stats)
91
+ .filter(([lang, stat]) => lang === 'en' || stat.translateds.size)
92
+ .map(([lang, stat]) => pad(lang, 0) + pad(String(stat.untranslateds.size), 1).red + pad(String(stat.translateds.size), 2).green).join_lines(false));
93
+ const en_untranslateds = stats.en.untranslateds;
94
+ const n_en_untranslateds = en_untranslateds.size;
95
+ if (n_en_untranslateds) {
96
+ console.log('\n缺少英文翻译的词条:'.yellow);
97
+ let i = 0;
98
+ for (const untranslated of en_untranslateds) {
99
+ if (i >= 10)
100
+ break;
101
+ console.log(untranslated);
102
+ i++;
103
+ }
104
+ if (n_en_untranslateds > 10) {
105
+ console.log('...');
106
+ console.log(`--- 共 ${en_untranslateds.size} 个未翻译的英文词条 ---`);
107
+ }
108
+ }
109
+ else
110
+ console.log('\n所有词条都至少含有英文翻译'.green);
111
+ // --- 生成 untranslateds.json,并自动翻译 (扫描到词条还没有英文翻译)
112
+ let untranslateds = {};
113
+ if (n_en_untranslateds) {
114
+ const _en_untranslateds = [...en_untranslateds];
115
+ for (let i = 0; i < _en_untranslateds.length; ++i) {
116
+ const key = _en_untranslateds[i];
117
+ let item = { ...dict.get(key) };
118
+ item.en ||= '';
119
+ item.ja ||= '';
120
+ item.ko ||= '';
121
+ untranslateds[key] = item;
122
+ }
123
+ }
124
+ // --- 保存到 dict.json, untranslateds.json
125
+ const fp_untranslateds = `${fpd_out}untranslateds.json`;
126
+ const fp_dict_ = `${fpd_out}dict.json`;
127
+ await Promise.all([
128
+ fwrite(fp_untranslateds, untranslateds, noprint),
129
+ fwrite(fp_dict_, dict.to_json(true) + '\n', noprint)
130
+ ]);
131
+ console.log((n_en_untranslateds ?
132
+ `${'请手动补全未翻译的词条: '.yellow}${fp_untranslateds.underline.blue}\n` +
133
+ '补全 untranslateds.json 后需要重新运行扫描,会根据 untranslateds.json 更新 dict.json\n'.yellow
134
+ :
135
+ '') +
136
+ `${'请检查词典文件: '}${fp_dict_.underline.blue}\n` +
137
+ '最后词典文件中词条会被打包进 js, 通过 new I18N(<dict.json>) 或 i18n.init(<dict.json>) 加载\n');
138
+ return n_en_untranslateds;
139
+ }
140
+ function pad(str, index) {
141
+ return str.pad(widths[index], { position: 'left' });
142
+ }
143
+ const widths = [6, 8, 8];
144
+ const excludes = [
145
+ 'node_modules/',
146
+ 'out/',
147
+ 'dist/',
148
+ ];
16
149
  /** 默认 i18next 扫描配置 */
17
- const DEFAULT_CONFIG = {
150
+ const default_config = {
18
151
  debug: false,
19
- input: [
20
- // 'src/**/*.{js,jsx,ts,tsx}',
21
- '!i18n/**', // Use ! to filter out files or directories
22
- '!node_modules/**',
23
- '!**/*.d.ts',
24
- ],
152
+ input: [],
25
153
  // 相对于根目录
26
154
  output: 'i18n/',
27
155
  // 若是相对路径,则以 output 为基准进行解析
@@ -31,7 +159,7 @@ const DEFAULT_CONFIG = {
31
159
  defaultLng: 'zh',
32
160
  defaultNs: 'translation',
33
161
  func: {
34
- list: ['i18next.t', 'i18n.t', 'i18n.__', 't', '__'],
162
+ list: ['t'],
35
163
  extensions: [], // 避免在 transform 中执行原生的 parseFuncFromString
36
164
  },
37
165
  trans: {
@@ -93,245 +221,4 @@ const DEFAULT_CONFIG = {
93
221
  suffix: '}}' // suffix for interpolation
94
222
  }
95
223
  };
96
- const VALID_EXTENTIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
97
- /** 扫描源码中的词条,以及收集未翻译的词条,将结果保存到 dict.json 和 untranslateds.json
98
- - `process.cwd()` rootdir 要扫描根目录
99
- - config 配置信息
100
- */
101
- export async function scanner(rootdir = path.normalize(process.cwd()), config = {}) {
102
- const output = path.resolve(rootdir, config.output || DEFAULT_CONFIG.output);
103
- if (!config.input.length)
104
- throw new Error('运行 i18n-scan 请指定 --input');
105
- const input = [...config.input, ...DEFAULT_CONFIG.input];
106
- config = {
107
- ...DEFAULT_CONFIG,
108
- ...config,
109
- input,
110
- output,
111
- resource: {
112
- loadPath: '',
113
- savePath: path.resolve(output, 'translation/{{lng}}.js'),
114
- jsonIndent: 4,
115
- lineEnding: '\n'
116
- }
117
- };
118
- let dict = new RWDict();
119
- for (const fp_dict of config.dict)
120
- dict.merge(await try_load_dict(path.resolve(output, fp_dict)), { print: false, overwrite: true });
121
- let c_files = 0;
122
- let c_scanneds = 0;
123
- let error_handlers = [];
124
- // 所有语言的扫描统计信息
125
- let stats = {};
126
- for (const language of LANGUAGES)
127
- stats[language] = {
128
- translateds: new Set(),
129
- untranslateds: new Set()
130
- };
131
- let spinner = ora({ interval: 66 }).start('Scanning...');
132
- function on_scanned(text, { language, key, defaultValue, count, context }) {
133
- // console.log(text, { language, key, defaultValue, count, context })
134
- text = text || defaultValue;
135
- if (!key)
136
- key = context ? `${text}_${context}` : text;
137
- if (!language) {
138
- for (const language of LANGUAGES)
139
- on_scanned(text, { language, key, count, context });
140
- return;
141
- }
142
- // console.log(text, { language, key, defaultValue, count, context })
143
- // debugger
144
- const stat = stats[language];
145
- // 获取已有翻译
146
- const translation = dict.get(key, language) ||
147
- language === 'zh' && text ||
148
- '';
149
- if (language === 'zh' && !context)
150
- return;
151
- if (translation)
152
- stat.translateds.add(key);
153
- else
154
- stat.untranslateds.add(key);
155
- if (language === 'en' && count !== undefined)
156
- on_scanned(text, { language, key: `${key}_plural`, context });
157
- }
158
- function new_vinyl_file(_path, data) {
159
- return new Vinyl({
160
- cwd: rootdir,
161
- base: rootdir,
162
- path: path.resolve(config.output, _path),
163
- contents: Buffer.from(typeof data === 'string' ? data : JSON.stringify(data, null, 4))
164
- });
165
- }
166
- return new Promise((resolve, reject) => {
167
- // ------------ scan by file
168
- vfs
169
- .src(config.input, { cwd: rootdir, sync: false })
170
- // 每个文件扫描前,统计文件数量
171
- .pipe(map_stream((file, cb) => {
172
- // 支持 `// @i18n-noscan` 忽略扫描
173
- if (/\/\/\s*@i18n-noscan\s/.test(file.contents.toString()))
174
- return cb();
175
- c_files++;
176
- cb(null, file);
177
- }))
178
- // 对文件进行排序,保证词条有一定的顺序
179
- .pipe(sort())
180
- // 分析代码提取词条
181
- .pipe(i18n_scanner.createStream(config, function transform(file, encoding, callback) {
182
- const { parser } = this;
183
- const ext = path.extname(file.path);
184
- // 只扫描源码文件
185
- if (!VALID_EXTENTIONS.has(ext)) {
186
- callback();
187
- return;
188
- }
189
- c_scanneds++;
190
- const percent = Math.round(100 * c_scanneds / c_files);
191
- const text = `Scanning (${percent}%): ${file.path.blue}`;
192
- spinner.text = cli_truncate(text, process.stdout.columns - 5, { position: 'middle', });
193
- let code = file.contents.toString();
194
- // --- 添加代码中扫描到的 i18n.t('key') 中的 key 到 parser
195
- // parser.parseFuncFromString 使用 esprima 来解析代码,esprima 仍然不支持 optional chaining !!
196
- parser.parseFuncFromString(code.replace(/\?\.\[/g, '[').replace(/\?\.\(/g, '(').replace(/\?\./g, '.'), on_scanned);
197
- // --- 添加代码中扫描到的 Trans 组件中的 key 到 parser
198
- if (ext === '.jsx' || ext === '.tsx') {
199
- // parser.parseTransFromString 使用 acorn 解析代码,不支持 TypeScript,添加 parser.parseTransFromStringByBabel
200
- mix_parse_trans_from_string_by_babel(parser);
201
- parser.parseTransFromStringByBabel(code, { filepath: file.path }, on_scanned, (error) => {
202
- error_handlers.push(error);
203
- });
204
- }
205
- setTimeout(callback, 0);
206
- }))
207
- // 创建词条文件
208
- .pipe(through2.obj(
209
- /** i18n-scanner 会把扫描结果以每个语言一个文件的形式提供,这里解析扫描结果
210
- * file: 翻译 resource 文件,其中 file.contents 包含翻译的扫描结果
211
- */
212
- function write(file, encoding, cb) { cb(); },
213
- /** 生成 stats.json, unmarkeds.md; 打印 untranslated / unmarkeds */
214
- function flush(cb) {
215
- // ------------ stats.json
216
- // this.push(new_vinyl_file('stats.json',
217
- // Object.fromEntries(
218
- // Object.entries(stats).map( ([l, { translateds, untranslateds }]) =>
219
- // [l, { translateds: Array.from(translateds), untranslateds: Array.from(untranslateds) }])
220
- // )
221
- // ))
222
- // ------------ 打印 cli 统计表
223
- const table = new CliTable({
224
- head: [
225
- '语言',
226
- '未翻译'.red,
227
- '已翻译'.green,
228
- ],
229
- colAligns: ['right', 'right', 'right', 'right'],
230
- style: { head: [] },
231
- chars: {
232
- top: '',
233
- 'top-mid': '',
234
- 'top-left': '',
235
- 'top-right': '',
236
- bottom: '',
237
- 'bottom-mid': '',
238
- 'bottom-left': '',
239
- 'bottom-right': '',
240
- left: '',
241
- 'left-mid': '',
242
- mid: '',
243
- 'mid-mid': '',
244
- right: '',
245
- 'right-mid': '',
246
- middle: ' ',
247
- },
248
- });
249
- Object.entries(stats).forEach(([lang, stat]) => {
250
- table.push([
251
- lang,
252
- String(stat.untranslateds.size).red,
253
- String(stat.translateds.size).green
254
- ]);
255
- });
256
- spinner.stop();
257
- console.log(`Scanned ${c_files} files. Occured ${error_handlers.length} errors.`);
258
- console.log(table.toString());
259
- // ------------ 生成 unmarkeds.md 统计
260
- /*
261
- const fp_unmarked = path.resolve(config.output, 'unmarkeds.md')
262
-
263
- if (fs.existsSync(fp_unmarked))
264
- rimraf.sync(fp_unmarked)
265
-
266
- if (unmarkeds.length) {
267
- console.log(colors.yellow(`\n⚠️ 发现未标记的中文字符 ${unmarkeds.length} 处:\n`))
268
- unmarkeds.forEach(({ value, filepath, loc: { start } }, index) => {
269
- if (index >= 5) return
270
- console.log( ` ${colors.white(`'${value}'`)}\t${colors.blue.underline(`${path.relative(rootdir, filepath)}:${start.line}:${start.column + 1}`)}` )
271
- })
272
- }
273
-
274
- this.push( new_vinyl_file( fp_unmarked,
275
- unmarkeds.map( ({ value, filepath, loc }) =>
276
- '- [' + value.trim() + '](' + path.relative( config.output, path.resolve(rootdir, filepath || '') ) + '#L' + loc.start.line + ')'
277
- ).join('\n') + '\n'
278
- ))
279
-
280
-
281
- if (unmarkeds.length > 5) {
282
- console.log(' ...')
283
- console.log(colors.yellow(`\n 完整未标记词条请查看 ${colors.blue.underline(path.relative(rootdir, fp_unmarked))}`))
284
- }
285
- */
286
- const en_untranslateds = stats.en.untranslateds;
287
- if (en_untranslateds.size) {
288
- console.log('\n缺少英文翻译的词条:'.yellow);
289
- let i = 0;
290
- for (const untranslated of en_untranslateds) {
291
- if (i >= 10)
292
- break;
293
- console.log(untranslated);
294
- i++;
295
- }
296
- if (en_untranslateds.size > 10) {
297
- console.log('...');
298
- console.log(`--- 共 ${en_untranslateds.size} 个未翻译的英文词条 ---`);
299
- }
300
- }
301
- else
302
- console.log('\n所有词条都至少含有英文翻译'.green);
303
- // ------------ 生成 untranslateds.json (扫描到词条还没有英文翻译)
304
- const fp_untranslateds = path.resolve(config.output, 'untranslateds.json');
305
- let untranslateds = {};
306
- for (const key of stats.en.untranslateds) {
307
- let item = { ...dict.get(key) };
308
- item.en ||= '';
309
- item.ja ||= '';
310
- item.ko ||= '';
311
- untranslateds[key] = item;
312
- }
313
- this.push(new_vinyl_file(fp_untranslateds, untranslateds));
314
- // ------------ 写入 dict.json
315
- const fp_dict_new = path.resolve(output, 'dict.json');
316
- this.push(new_vinyl_file(fp_dict_new, dict.to_json(true) + '\n'));
317
- console.log(`\n\n${'请手动补全未翻译的词条: '.yellow}${fp_untranslateds.underline.blue}\n` +
318
- `${'请检查新生成的词典文件: '.yellow}${fp_dict_new.underline.blue}\n` +
319
- '\n' +
320
- '补全 untranslateds.json 后需要重新运行扫描,会根据 untranslateds.json 更新 dict.json\n'.yellow +
321
- '最后 dict.json 所包含的词条会被打包进 js, 通过 new I18N(<dict.json>) 或 i18n.init(<dict.json>) 加载\n\n'.yellow +
322
- `${'详细文档请查看: '.yellow}${'https://github.com/ShenHongFei/xshell/tree/master/i18n'.blue.underline}`);
323
- cb();
324
- }))
325
- // 写入词条文件
326
- .pipe(vfs.dest(rootdir))
327
- .on('end', () => {
328
- if (error_handlers.length) {
329
- for (const error_handler of error_handlers)
330
- error_handler();
331
- console.log(`以上错误可能是由不规范的词条标记导致,标记规范可见:\n${'https://www.i18next.com/translation-function/essentials'.blue.underline}`);
332
- }
333
- resolve(stats.en.untranslateds.size);
334
- });
335
- });
336
- }
337
224
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
1
  import '../../prototype.ts';
2
- /** file:///D:/0/i18next-scanner/src/parser.js */
3
- export declare function mix_parse_trans_from_string_by_babel(parser: any): void;
2
+ /** i18next-scanner/src/parser.js */
3
+ export declare function parse_trans_from_string_by_babel(this: any, code: string, options?: {}, custom_handler?: any, on_error?: (callback: Function) => void): any;
@@ -5,109 +5,107 @@ import { parse } from '@babel/parser';
5
5
  import t from '@babel/types';
6
6
  import "../../prototype.js";
7
7
  // import { Checker } from './checker'
8
- /** file:///D:/0/i18next-scanner/src/parser.js */
9
- export function mix_parse_trans_from_string_by_babel(parser) {
10
- parser.parseTransFromStringByBabel = function parse_trans_from_string_by_babel(code, options = {}, custom_handler = null, on_error = () => { }) {
11
- if (typeof options === 'function') {
12
- custom_handler = options;
13
- options = {};
14
- }
15
- const { transformOptions = {}, // object
16
- component = this.options.trans.component, // string
17
- i18nKey = this.options.trans.i18nKey, // string
18
- defaultsKey = this.options.trans.defaultsKey, // string
19
- fallbackKey = this.options.trans.fallbackKey, // boolean|function
20
- babylon: babylon_options = this.options.trans.babylon, // object
21
- filepath, } = options;
22
- const parseJSXElement = ({ node }) => {
23
- if (!node)
24
- return;
25
- if (node.openingElement.name.name !== component)
26
- return;
27
- const getLiteralValue = literal => {
28
- if (t.isTemplateLiteral(literal))
29
- return literal.quasis.map(element => element.value.cooked).join('');
30
- return literal.value;
31
- };
32
- const attr = castArray(node.openingElement.attributes).reduce((acc, attribute) => {
33
- if (!t.isJSXAttribute(attribute) ||
34
- !t.isJSXIdentifier(attribute.name))
35
- return acc;
36
- const { name } = attribute.name;
37
- const value = attribute.value;
38
- if (t.isLiteral(value))
39
- acc[name] = getLiteralValue(value);
40
- else if (t.isJSXExpressionContainer(value)) {
41
- const expression = value.expression;
42
- if (t.isIdentifier(expression))
43
- acc[name] = expression.name;
44
- else if (t.isLiteral(expression))
45
- acc[name] = getLiteralValue(expression);
46
- else if (t.isObjectExpression(expression)) {
47
- const properties = castArray(expression.properties);
48
- acc[name] = properties.reduce((obj, property) => {
49
- if (!t.isObjectProperty(property))
50
- return obj;
51
- if (t.isLiteral(property.value))
52
- obj[property.key.name] = getLiteralValue(property.value);
53
- else // Unable to get value of the property
54
- obj[property.key.name] = '';
8
+ /** i18next-scanner/src/parser.js */
9
+ export function parse_trans_from_string_by_babel(code, options = {}, custom_handler = null, on_error = () => { }) {
10
+ if (typeof options === 'function') {
11
+ custom_handler = options;
12
+ options = {};
13
+ }
14
+ const { transformOptions = {}, // object
15
+ component = this.options.trans.component, // string
16
+ i18nKey = this.options.trans.i18nKey, // string
17
+ defaultsKey = this.options.trans.defaultsKey, // string
18
+ fallbackKey = this.options.trans.fallbackKey, // boolean|function
19
+ babylon: babylon_options = this.options.trans.babylon, // object
20
+ filepath, } = options;
21
+ const parseJSXElement = ({ node }) => {
22
+ if (!node)
23
+ return;
24
+ if (node.openingElement.name.name !== component)
25
+ return;
26
+ const getLiteralValue = literal => {
27
+ if (t.isTemplateLiteral(literal))
28
+ return literal.quasis.map(element => element.value.cooked).join('');
29
+ return literal.value;
30
+ };
31
+ const attr = cast_array(node.openingElement.attributes).reduce((acc, attribute) => {
32
+ if (!t.isJSXAttribute(attribute) ||
33
+ !t.isJSXIdentifier(attribute.name))
34
+ return acc;
35
+ const { name } = attribute.name;
36
+ const value = attribute.value;
37
+ if (t.isLiteral(value))
38
+ acc[name] = getLiteralValue(value);
39
+ else if (t.isJSXExpressionContainer(value)) {
40
+ const expression = value.expression;
41
+ if (t.isIdentifier(expression))
42
+ acc[name] = expression.name;
43
+ else if (t.isLiteral(expression))
44
+ acc[name] = getLiteralValue(expression);
45
+ else if (t.isObjectExpression(expression)) {
46
+ const properties = cast_array(expression.properties);
47
+ acc[name] = properties.reduce((obj, property) => {
48
+ if (!t.isObjectProperty(property))
55
49
  return obj;
56
- }, {});
57
- /** 防止 count 被忽略,如
58
- ```jsx
59
- <Trans count={arr.length}>
60
- 一二三{{ count: arr.length }}
61
- </Trans>
62
- ``` */
63
- }
64
- else if (name === 'count')
65
- acc[name] = 0;
50
+ if (t.isLiteral(property.value))
51
+ obj[property.key.name] = getLiteralValue(property.value);
52
+ else // Unable to get value of the property
53
+ obj[property.key.name] = '';
54
+ return obj;
55
+ }, {});
56
+ /** 防止 count 被忽略,如
57
+ ```jsx
58
+ <Trans count={arr.length}>
59
+ 一二三{{ count: arr.length }}
60
+ </Trans>
61
+ ``` */
66
62
  }
67
- return acc;
68
- }, {});
69
- const transKey = attr[i18nKey].trim();
70
- const defaultsString = attr[defaultsKey] || '';
71
- if (typeof defaultsString !== 'string')
72
- this.log(`defaults value must be a static string, saw ${defaultsString.yellow}`);
73
- // https://www.i18next.com/translation-function/essentials#overview-options
74
- const tOptions = attr.tOptions;
75
- const options = {
76
- ...tOptions,
77
- defaultValue: defaultsString || nodes_to_string(node.children, filepath, on_error),
78
- fallbackKey,
79
- };
80
- if (Object.prototype.hasOwnProperty.call(attr, 'count'))
81
- options.count = Number(attr.count) || 0;
82
- if (Object.prototype.hasOwnProperty.call(attr, 'ns')) {
83
- if (typeof options.ns !== 'string')
84
- this.log(`The ns attribute must be a string, saw ${attr.ns?.yellow}`);
85
- options.ns = attr.ns;
86
- }
87
- if (custom_handler) {
88
- custom_handler(transKey, options);
89
- return;
63
+ else if (name === 'count')
64
+ acc[name] = 0;
90
65
  }
91
- this.set(transKey, options);
66
+ return acc;
67
+ }, {});
68
+ const transKey = attr[i18nKey]?.trim();
69
+ const defaultsString = attr[defaultsKey] || '';
70
+ if (typeof defaultsString !== 'string')
71
+ this.log(`defaults value must be a static string, saw ${defaultsString.yellow}`);
72
+ // https://www.i18next.com/translation-function/essentials#overview-options
73
+ const tOptions = attr.tOptions;
74
+ const options = {
75
+ ...tOptions,
76
+ defaultValue: defaultsString || nodes_to_string(node.children, filepath, on_error),
77
+ fallbackKey,
92
78
  };
93
- try {
94
- const ast = parse(code, { ...babylon_options });
95
- traverse(ast, { JSXElement: parseJSXElement, });
96
- // traverse(ast, Checker({ filepath }))
79
+ if (Object.prototype.hasOwnProperty.call(attr, 'count'))
80
+ options.count = Number(attr.count) || 0;
81
+ if (Object.prototype.hasOwnProperty.call(attr, 'ns')) {
82
+ if (typeof options.ns !== 'string')
83
+ this.log(`The ns attribute must be a string, saw ${attr.ns?.yellow}`);
84
+ options.ns = attr.ns;
97
85
  }
98
- catch (err) {
99
- on_error(() => {
100
- console.error('');
101
- const { line, column } = (err && err.loc) || { line: 1, column: 1 };
102
- console.error([filepath, line, column].join(':').yellow);
103
- console.error(`Unable to parse ${component?.blue} component.\n`.red);
104
- if (!filepath)
105
- console.error(String(code).red);
106
- console.error((' ' + err.message).red);
107
- });
86
+ if (custom_handler) {
87
+ custom_handler(transKey, options);
88
+ return;
108
89
  }
109
- return this;
90
+ this.set(transKey, options);
110
91
  };
92
+ try {
93
+ const ast = parse(code, { ...babylon_options });
94
+ traverse(ast, { JSXElement: parseJSXElement, });
95
+ // traverse(ast, Checker({ filepath }))
96
+ }
97
+ catch (err) {
98
+ on_error(() => {
99
+ console.error('');
100
+ const { line, column } = (err && err.loc) || { line: 1, column: 1 };
101
+ console.error([filepath, line, column].join(':').yellow);
102
+ console.error(`Unable to parse ${component?.blue} component.\n`.red);
103
+ if (!filepath)
104
+ console.error(String(code).red);
105
+ console.error((' ' + err.message).red);
106
+ });
107
+ }
108
+ return this;
111
109
  }
112
110
  function nodes_to_string(nodes, filepath, onError) {
113
111
  let memo = '';
@@ -145,7 +143,7 @@ function nodes_to_string(nodes, filepath, onError) {
145
143
  });
146
144
  return memo;
147
145
  }
148
- function castArray(value) {
146
+ function cast_array(value) {
149
147
  return Array.isArray(value) ? value : [value];
150
148
  }
151
149
  //# sourceMappingURL=parser.js.map
package/net.d.ts CHANGED
@@ -297,5 +297,5 @@ export declare class RemoteClient {
297
297
  [inspect.custom](): {
298
298
  remote: string;
299
299
  websocket: string;
300
- } & Omit<this, typeof import("util").inspect.custom | "websocket" | "remote" | "call" | "send">;
300
+ } & Omit<this, "websocket" | typeof import("util").inspect.custom | "call" | "remote" | "send">;
301
301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.2.47",
3
+ "version": "1.2.48",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -57,20 +57,17 @@
57
57
  "@svgr/webpack": "^8.1.0",
58
58
  "@types/sass-loader": "^8.0.9",
59
59
  "@types/ws": "^8.18.1",
60
- "@typescript-eslint/eslint-plugin": "^8.33.1",
61
- "@typescript-eslint/parser": "^8.33.1",
62
- "@typescript-eslint/utils": "^8.33.1",
60
+ "@typescript-eslint/eslint-plugin": "^8.34.0",
61
+ "@typescript-eslint/parser": "^8.34.0",
62
+ "@typescript-eslint/utils": "^8.34.0",
63
63
  "archiver": "^7.0.1",
64
64
  "chalk": "^5.4.1",
65
- "cli-table3": "^0.6.5",
66
- "cli-truncate": "^4.0.0",
67
65
  "commander": "^14.0.0",
68
66
  "css-loader": "^7.1.2",
69
67
  "emoji-regex": "^10.4.0",
70
68
  "eslint": "^9.28.0",
71
69
  "eslint-plugin-import": "^2.31.0",
72
70
  "eslint-plugin-react": "^7.37.5",
73
- "gulp-sort": "^2.0.0",
74
71
  "https-proxy-agent": "^7.0.6",
75
72
  "i18next": "^25.2.1",
76
73
  "i18next-scanner": "^4.6.0",
@@ -79,25 +76,21 @@
79
76
  "license-webpack-plugin": "^4.0.2",
80
77
  "map-stream": "^0.0.7",
81
78
  "mime-types": "^3.0.1",
82
- "ora": "^8.2.0",
83
79
  "react": "^19.1.0",
84
80
  "react-i18next": "^15.5.2",
85
81
  "react-object-model": "^1.2.24",
86
82
  "resolve-path": "^1.4.0",
87
- "sass": "^1.89.1",
83
+ "sass": "^1.89.2",
88
84
  "sass-loader": "^16.0.5",
89
85
  "source-map-loader": "^5.0.0",
90
86
  "strip-ansi": "^7.1.0",
91
87
  "style-loader": "^4.0.0",
92
- "through2": "^4.0.2",
93
88
  "tough-cookie": "^5.1.2",
94
89
  "ts-loader": "^9.5.2",
95
90
  "tslib": "^2.8.1",
96
91
  "typescript": "^5.8.3",
97
92
  "ua-parser-js": "^2.0.3",
98
93
  "undici": "^7.10.0",
99
- "vinyl": "^3.0.1",
100
- "vinyl-fs": "^4.0.2",
101
94
  "webpack": "^5.99.9",
102
95
  "webpack-bundle-analyzer": "^4.10.2",
103
96
  "ws": "^8.18.2"
@@ -107,17 +100,14 @@
107
100
  "@types/archiver": "^6.0.3",
108
101
  "@types/babel__traverse": "^7.20.7",
109
102
  "@types/eslint": "^9.6.1",
110
- "@types/estree": "^1.0.7",
111
- "@types/gulp-sort": "^2.0.4",
103
+ "@types/estree": "^1.0.8",
112
104
  "@types/koa": "^2.15.0",
113
105
  "@types/koa-compress": "^4.0.6",
114
- "@types/mime-types": "^3.0.0",
115
- "@types/node": "^22.15.30",
116
- "@types/react": "^19.1.6",
117
- "@types/through2": "^2.0.41",
106
+ "@types/mime-types": "^3.0.1",
107
+ "@types/node": "^24.0.0",
108
+ "@types/react": "^19.1.7",
118
109
  "@types/tough-cookie": "^4.0.5",
119
110
  "@types/ua-parser-js": "^0.7.39",
120
- "@types/vinyl-fs": "^3.0.6",
121
111
  "@types/vscode": "^1.100.0",
122
112
  "@types/webpack-bundle-analyzer": "^4.7.0"
123
113
  }
package/server.js CHANGED
@@ -706,9 +706,13 @@ export class Server {
706
706
  return fp;
707
707
  }
708
708
  set_content_type(response, fext) {
709
- response.set('content-type', (Server.js_exts.has(fext)
710
- ? 'text/javascript; chatset=utf-8'
711
- : get_content_type(`file.${fext}`) || 'application/octet-stream'));
709
+ response.set('content-type', (Server.js_exts.has(fext) ?
710
+ 'text/javascript; chatset=utf-8'
711
+ :
712
+ fext === 'mp4' ?
713
+ 'video/mp4'
714
+ :
715
+ get_content_type(`file.${fext}`) || 'application/octet-stream'));
712
716
  }
713
717
  /** - range: 取值为逗号分割的多个可用端口或端口区间 (不能含有空格,包含区间右值),比如:`8321,8322,8300-8310,11000-11999
714
718
  - reverse?: `false` 在 range 内从后往前尝试 */
@@ -137,3 +137,7 @@ export declare function ceil2(n: number): number;
137
137
  export declare function throttle(duration: number, func: Function, delay_first?: boolean): (this: any, ...args: any[]) => void;
138
138
  /** 防抖,间隔一段时间不再触发时调用 */
139
139
  export declare function debounce(duration: number, func: Function): (this: any, ...args: any[]) => void;
140
+ /** 轮询尝试 action 共 times 次,每次间隔 duration
141
+ action 返回 trusy 值时认为成功,返回 action 的结果
142
+ 如果次数用尽仍然失败,返回 null */
143
+ export declare function poll<TResult>(duration: number, times: number, action: (breaker: () => void) => Promise<TResult>): Promise<TResult>;
package/utils.browser.js CHANGED
@@ -461,4 +461,22 @@ export function debounce(duration, func) {
461
461
  }, duration);
462
462
  };
463
463
  }
464
+ /** 轮询尝试 action 共 times 次,每次间隔 duration
465
+ action 返回 trusy 值时认为成功,返回 action 的结果
466
+ 如果次数用尽仍然失败,返回 null */
467
+ export async function poll(duration, times, action) {
468
+ let break_flag = false;
469
+ function _break() {
470
+ break_flag = true;
471
+ }
472
+ for (let i = 0; i < times; ++i) {
473
+ const result = await action(_break);
474
+ if (result)
475
+ return result;
476
+ if (break_flag)
477
+ break;
478
+ await delay(duration);
479
+ }
480
+ return null;
481
+ }
464
482
  //# sourceMappingURL=utils.browser.js.map
package/utils.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Writable, Transform, type Readable, type Duplex, type TransformCallback } from 'stream';
2
2
  import util from 'util';
3
3
  import type { TimerOptions } from 'timers';
4
- import type Vinyl from 'vinyl';
5
4
  import { type Mapper } from './prototype.ts';
6
5
  /** `180` 输出字符宽度 */
7
6
  export declare const output_width = 180;
@@ -177,7 +176,7 @@ export declare namespace inspect {
177
176
  create an event stream and apply function to each .write,
178
177
  emitting each response as data unless it's an empty callback
179
178
  */
180
- export declare function map_stream<Out, In = Vinyl>(mapper: (obj: In, cb: Function) => any, options?: {
179
+ export declare function map_stream<Out, In = any>(mapper: (obj: In, cb: Function) => any, options?: {
181
180
  failures?: boolean;
182
181
  }): Duplex;
183
182
  export declare function stream_to_lines(stream: Readable): AsyncGenerator<string, void, unknown>;
@@ -214,3 +213,7 @@ export declare function ceil2(n: number): number;
214
213
  export declare function throttle(duration: number, func: Function, delay_first?: boolean): (this: any, ...args: any[]) => void;
215
214
  /** 防抖,间隔一段时间不再触发时调用 */
216
215
  export declare function debounce(duration: number, func: Function): (this: any, ...args: any[]) => void;
216
+ /** 轮询尝试 action 共 times 次,每次间隔 duration
217
+ action 返回 trusy 值时认为成功,返回 action 的结果
218
+ 如果次数用尽仍然失败,返回 null */
219
+ export declare function poll<TResult>(duration: number, times: number, action: (breaker: () => void) => Promise<TResult>): Promise<TResult>;
package/utils.js CHANGED
@@ -737,4 +737,22 @@ export function debounce(duration, func) {
737
737
  }, duration);
738
738
  };
739
739
  }
740
+ /** 轮询尝试 action 共 times 次,每次间隔 duration
741
+ action 返回 trusy 值时认为成功,返回 action 的结果
742
+ 如果次数用尽仍然失败,返回 null */
743
+ export async function poll(duration, times, action) {
744
+ let break_flag = false;
745
+ function _break() {
746
+ break_flag = true;
747
+ }
748
+ for (let i = 0; i < times; ++i) {
749
+ const result = await action(_break);
750
+ if (result)
751
+ return result;
752
+ if (break_flag)
753
+ break;
754
+ await delay(duration);
755
+ }
756
+ return null;
757
+ }
740
758
  //# sourceMappingURL=utils.js.map