xshell 1.2.47 → 1.2.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/builder.js +3 -0
- package/i18n/i18n-scan.js +6 -15
- package/i18n/index.d.ts +1 -1
- package/i18n/index.js +1 -1
- package/i18n/scanner/index.d.ts +7 -8
- package/i18n/scanner/index.js +147 -260
- package/i18n/scanner/parser.d.ts +2 -2
- package/i18n/scanner/parser.js +95 -97
- package/net.d.ts +1 -1
- package/package.json +15 -25
- package/prototype.browser.d.ts +1 -1
- package/prototype.browser.js +5 -5
- package/prototype.d.ts +1 -1
- package/prototype.js +5 -5
- package/server.js +7 -3
- package/utils.browser.d.ts +4 -0
- package/utils.browser.js +21 -3
- package/utils.d.ts +5 -2
- package/utils.js +21 -3
- package/xlint.js +1 -1
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
|
-
|
|
6
|
-
(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
3
|
+
export const languages = ['zh', 'en', 'ja', 'ko'];
|
|
4
4
|
/**
|
|
5
5
|
提供翻译文本功能,自动解析当前语言
|
|
6
6
|
@see https://github.com/ShenHongFei/xshell/tree/master/i18n
|
package/i18n/scanner/index.d.ts
CHANGED
|
@@ -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
|
|
7
|
+
declare const default_config: {
|
|
4
8
|
debug: boolean;
|
|
5
|
-
input:
|
|
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
|
|
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 {};
|
package/i18n/scanner/index.js
CHANGED
|
@@ -1,27 +1,155 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
12
|
-
import {
|
|
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 {
|
|
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
|
|
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: ['
|
|
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
|
package/i18n/scanner/parser.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '../../prototype.ts';
|
|
2
|
-
/**
|
|
3
|
-
export declare function
|
|
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;
|
package/i18n/scanner/parser.js
CHANGED
|
@@ -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
|
-
/**
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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 | "
|
|
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.
|
|
3
|
+
"version": "1.2.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -53,24 +53,21 @@
|
|
|
53
53
|
"@babel/parser": "^7.27.5",
|
|
54
54
|
"@babel/traverse": "^7.27.4",
|
|
55
55
|
"@koa/cors": "^5.0.0",
|
|
56
|
-
"@stylistic/eslint-plugin": "^
|
|
56
|
+
"@stylistic/eslint-plugin": "^5.0.0",
|
|
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.
|
|
61
|
-
"@typescript-eslint/parser": "^8.
|
|
62
|
-
"@typescript-eslint/utils": "^8.
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
62
|
+
"@typescript-eslint/utils": "^8.35.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
|
-
"eslint": "^9.
|
|
71
|
-
"eslint-plugin-import": "^2.
|
|
68
|
+
"eslint": "^9.29.0",
|
|
69
|
+
"eslint-plugin-import": "^2.32.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
|
-
"react-i18next": "^15.5.
|
|
80
|
+
"react-i18next": "^15.5.3",
|
|
85
81
|
"react-object-model": "^1.2.24",
|
|
86
82
|
"resolve-path": "^1.4.0",
|
|
87
|
-
"sass": "^1.89.
|
|
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
|
-
"ua-parser-js": "^2.0.
|
|
92
|
+
"ua-parser-js": "^2.0.4",
|
|
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,18 +100,15 @@
|
|
|
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.
|
|
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.
|
|
115
|
-
"@types/node": "^
|
|
116
|
-
"@types/react": "^19.1.
|
|
117
|
-
"@types/through2": "^2.0.41",
|
|
106
|
+
"@types/mime-types": "^3.0.1",
|
|
107
|
+
"@types/node": "^24.0.4",
|
|
108
|
+
"@types/react": "^19.1.8",
|
|
118
109
|
"@types/tough-cookie": "^4.0.5",
|
|
119
110
|
"@types/ua-parser-js": "^0.7.39",
|
|
120
|
-
"@types/
|
|
121
|
-
"@types/vscode": "^1.100.0",
|
|
111
|
+
"@types/vscode": "^1.101.0",
|
|
122
112
|
"@types/webpack-bundle-analyzer": "^4.7.0"
|
|
123
113
|
}
|
|
124
114
|
}
|
package/prototype.browser.d.ts
CHANGED
|
@@ -216,7 +216,7 @@ interface SliceOptions {
|
|
|
216
216
|
export declare const emoji_regex: RegExp;
|
|
217
217
|
export declare const noop: () => void;
|
|
218
218
|
export declare const ident: <T>(x: T) => T;
|
|
219
|
-
export declare const
|
|
219
|
+
export declare const select: <TObj = any, TKey extends keyof TObj = keyof TObj>(key: TKey) => (obj: TObj) => TObj[TKey];
|
|
220
220
|
export type Mapper<TObj = any, TKey extends keyof TObj = keyof TObj> = (obj: TObj) => TObj[TKey];
|
|
221
221
|
/** value 不为 null 或 undefined */
|
|
222
222
|
export declare const not_empty: (value: any) => boolean;
|
package/prototype.browser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { t } from "./i18n/instance.js";
|
|
|
4
4
|
export const emoji_regex = EmojiRegex();
|
|
5
5
|
export const noop = () => { };
|
|
6
6
|
export const ident = (x) => x;
|
|
7
|
-
export const
|
|
7
|
+
export const select = (key) => (obj) => obj[key];
|
|
8
8
|
/** value 不为 null 或 undefined */
|
|
9
9
|
export const not_empty = (value) => value !== null && value !== undefined;
|
|
10
10
|
export const empty = (value) => value === undefined || value === null;
|
|
@@ -509,7 +509,7 @@ Object.defineProperties(Array.prototype, {
|
|
|
509
509
|
if ((typeof first === 'number' || typeof first === 'bigint') && !mapper)
|
|
510
510
|
return this.reduce((acc, x) => acc + x, zero);
|
|
511
511
|
if (is_key_type(mapper))
|
|
512
|
-
mapper =
|
|
512
|
+
mapper = select(mapper);
|
|
513
513
|
mapper ??= ident;
|
|
514
514
|
return this.reduce((acc, x) => acc + mapper(x), zero);
|
|
515
515
|
},
|
|
@@ -517,7 +517,7 @@ Object.defineProperties(Array.prototype, {
|
|
|
517
517
|
if (!this.length)
|
|
518
518
|
return undefined;
|
|
519
519
|
if (is_key_type(mapper))
|
|
520
|
-
mapper =
|
|
520
|
+
mapper = select(mapper);
|
|
521
521
|
let max = mapper(this[0]);
|
|
522
522
|
let imax = 0;
|
|
523
523
|
for (let i = 0; i < this.length; i++) {
|
|
@@ -533,7 +533,7 @@ Object.defineProperties(Array.prototype, {
|
|
|
533
533
|
if (!this.length)
|
|
534
534
|
return undefined;
|
|
535
535
|
if (is_key_type(mapper))
|
|
536
|
-
mapper =
|
|
536
|
+
mapper = select(mapper);
|
|
537
537
|
let min = mapper(this[0]);
|
|
538
538
|
let imin = 0;
|
|
539
539
|
for (let i = 0; i < this.length; i++) {
|
|
@@ -549,7 +549,7 @@ Object.defineProperties(Array.prototype, {
|
|
|
549
549
|
if (!mapper)
|
|
550
550
|
return [...new Set(this)];
|
|
551
551
|
if (is_key_type(mapper))
|
|
552
|
-
mapper =
|
|
552
|
+
mapper = select(mapper);
|
|
553
553
|
let map = new Map();
|
|
554
554
|
for (const x of this)
|
|
555
555
|
map.set(mapper(x), x);
|
package/prototype.d.ts
CHANGED
|
@@ -246,7 +246,7 @@ interface SliceOptions {
|
|
|
246
246
|
export declare const emoji_regex: RegExp;
|
|
247
247
|
export declare const noop: () => void;
|
|
248
248
|
export declare const ident: <T>(x: T) => T;
|
|
249
|
-
export declare const
|
|
249
|
+
export declare const select: <TObj = any, TKey extends keyof TObj = keyof TObj>(key: TKey) => (obj: TObj) => TObj[TKey];
|
|
250
250
|
export type Mapper<TObj = any, TKey extends keyof TObj = keyof TObj> = (obj: TObj) => TObj[TKey];
|
|
251
251
|
/** value 不为 null 或 undefined */
|
|
252
252
|
export declare const not_empty: (value: any) => boolean;
|
package/prototype.js
CHANGED
|
@@ -5,7 +5,7 @@ import strip_ansi from 'strip-ansi';
|
|
|
5
5
|
import { t } from "./i18n/instance.js";
|
|
6
6
|
export const noop = () => { };
|
|
7
7
|
export const ident = (x) => x;
|
|
8
|
-
export const
|
|
8
|
+
export const select = (key) => (obj) => obj[key];
|
|
9
9
|
/** value 不为 null 或 undefined */
|
|
10
10
|
export const not_empty = (value) => value !== null && value !== undefined;
|
|
11
11
|
export const empty = (value) => value === undefined || value === null;
|
|
@@ -582,7 +582,7 @@ if (!globalThis.my_prototype_defined) {
|
|
|
582
582
|
if ((typeof first === 'number' || typeof first === 'bigint') && !mapper)
|
|
583
583
|
return this.reduce((acc, x) => acc + x, zero);
|
|
584
584
|
if (is_key_type(mapper))
|
|
585
|
-
mapper =
|
|
585
|
+
mapper = select(mapper);
|
|
586
586
|
mapper ??= ident;
|
|
587
587
|
return this.reduce((acc, x) => acc + mapper(x), zero);
|
|
588
588
|
},
|
|
@@ -590,7 +590,7 @@ if (!globalThis.my_prototype_defined) {
|
|
|
590
590
|
if (!this.length)
|
|
591
591
|
return undefined;
|
|
592
592
|
if (is_key_type(mapper))
|
|
593
|
-
mapper =
|
|
593
|
+
mapper = select(mapper);
|
|
594
594
|
let max = mapper(this[0]);
|
|
595
595
|
let imax = 0;
|
|
596
596
|
for (let i = 0; i < this.length; i++) {
|
|
@@ -606,7 +606,7 @@ if (!globalThis.my_prototype_defined) {
|
|
|
606
606
|
if (!this.length)
|
|
607
607
|
return undefined;
|
|
608
608
|
if (is_key_type(mapper))
|
|
609
|
-
mapper =
|
|
609
|
+
mapper = select(mapper);
|
|
610
610
|
let min = mapper(this[0]);
|
|
611
611
|
let imin = 0;
|
|
612
612
|
for (let i = 0; i < this.length; i++) {
|
|
@@ -622,7 +622,7 @@ if (!globalThis.my_prototype_defined) {
|
|
|
622
622
|
if (!mapper)
|
|
623
623
|
return [...new Set(this)];
|
|
624
624
|
if (is_key_type(mapper))
|
|
625
|
-
mapper =
|
|
625
|
+
mapper = select(mapper);
|
|
626
626
|
let map = new Map();
|
|
627
627
|
for (const x of this)
|
|
628
628
|
map.set(mapper(x), x);
|
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
|
-
|
|
711
|
-
:
|
|
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 内从后往前尝试 */
|
package/utils.browser.d.ts
CHANGED
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { is_key_type,
|
|
1
|
+
import { is_key_type, select, not_empty, ident } from "./prototype.browser.js";
|
|
2
2
|
import { t } from "./i18n/instance.js";
|
|
3
3
|
export function assert(assertion, message) {
|
|
4
4
|
if (!assertion) {
|
|
@@ -33,7 +33,7 @@ export function unique(iterable, mapper) {
|
|
|
33
33
|
if (!mapper)
|
|
34
34
|
return [...new Set(iterable)];
|
|
35
35
|
if (is_key_type(mapper))
|
|
36
|
-
mapper =
|
|
36
|
+
mapper = select(mapper);
|
|
37
37
|
let map = new Map();
|
|
38
38
|
for (const x of iterable)
|
|
39
39
|
map.set(mapper(x), x);
|
|
@@ -282,7 +282,7 @@ export function fuzzyfilter(query, list, mapper = ident, list_lower, single_char
|
|
|
282
282
|
if (!query)
|
|
283
283
|
return list;
|
|
284
284
|
if (is_key_type(mapper))
|
|
285
|
-
mapper =
|
|
285
|
+
mapper = select(mapper);
|
|
286
286
|
mapper ??= ident;
|
|
287
287
|
list_lower ??= list.map(item => mapper(item).toLowerCase());
|
|
288
288
|
const query_lower = query.toLowerCase();
|
|
@@ -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 =
|
|
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
|
@@ -2,7 +2,7 @@ import { Stream, Writable, Transform } from 'stream';
|
|
|
2
2
|
import util from 'util';
|
|
3
3
|
import timers from 'timers/promises';
|
|
4
4
|
import { t } from "./i18n/instance.js";
|
|
5
|
-
import {
|
|
5
|
+
import { select, not_empty, is_key_type, noop, ident } from "./prototype.js";
|
|
6
6
|
/** `180` 输出字符宽度 */
|
|
7
7
|
export const output_width = 180;
|
|
8
8
|
export const url_width = 52;
|
|
@@ -55,7 +55,7 @@ export function unique(iterable, mapper) {
|
|
|
55
55
|
if (!mapper)
|
|
56
56
|
return [...new Set(iterable)];
|
|
57
57
|
if (is_key_type(mapper))
|
|
58
|
-
mapper =
|
|
58
|
+
mapper = select(mapper);
|
|
59
59
|
let map = new Map();
|
|
60
60
|
for (const x of iterable)
|
|
61
61
|
map.set(mapper(x), x);
|
|
@@ -152,7 +152,7 @@ export function fuzzyfilter(query, list, mapper = ident, list_lower, single_char
|
|
|
152
152
|
if (!query)
|
|
153
153
|
return list;
|
|
154
154
|
if (is_key_type(mapper))
|
|
155
|
-
mapper =
|
|
155
|
+
mapper = select(mapper);
|
|
156
156
|
mapper ??= ident;
|
|
157
157
|
list_lower ??= list.map(item => mapper(item).toLowerCase());
|
|
158
158
|
const query_lower = query.toLowerCase();
|
|
@@ -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
|
package/xlint.js
CHANGED
|
@@ -809,7 +809,7 @@ export const xlint_config = {
|
|
|
809
809
|
// [a, b, c]
|
|
810
810
|
'@stylistic/comma-spacing': 'error',
|
|
811
811
|
// foo()
|
|
812
|
-
'@stylistic/
|
|
812
|
+
'@stylistic/function-call-spacing': 'error',
|
|
813
813
|
// a => { } 中箭头左右两边空格
|
|
814
814
|
'@stylistic/arrow-spacing': ['error'],
|
|
815
815
|
// 注释双斜杠后面要有空格
|