tcdona_unilib 1.0.0 → 1.0.2
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/@ast-grep.ts +18 -0
- package/README.md +19 -35
- package/dotenvx.ts +13 -0
- package/hono.ts +23 -0
- package/hotkeys-js.ts +2 -0
- package/inquirer.ts +13 -0
- package/package.json +27 -3
- package/pinyin-pro.ts +16 -0
- package/staticMeta/ast.scan.ts +131 -0
- package/staticMeta/ast.ts +259 -0
- package/staticMeta/eff.delay.ts +17 -0
- package/staticMeta/eff.test.ts +858 -0
- package/staticMeta/eff.ts +203 -0
- package/staticMeta/enum.api.ts +142 -0
- package/staticMeta/file.yml.ts +18 -0
- package/staticMeta/iduniq.ts +320 -0
- package/staticMeta/idupdate.ts +334 -0
- package/staticMeta/path.init.ts +21 -0
- package/staticMeta/pkg.json.ts +138 -0
- package/staticMeta/string.nanoid.ts +14 -0
- package/staticMeta/sync.ts +296 -0
- package/staticMeta/url.ts +110 -0
- package/tinypool.ts +27 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 目录同步工具:支持比较文件内容和修改时间,决定是否复制
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { err, ok } from 'neverthrow';
|
|
8
|
+
import { Eff } from './eff.js';
|
|
9
|
+
import type { ResultAsync } from 'neverthrow';
|
|
10
|
+
import { fileInit } from './enum.api.js';
|
|
11
|
+
|
|
12
|
+
// ==================== 日志配置 ====================
|
|
13
|
+
|
|
14
|
+
const fileId = fileInit('src/staticMeta/sync.ts');
|
|
15
|
+
|
|
16
|
+
// ==================== 类型定义 ====================
|
|
17
|
+
|
|
18
|
+
export interface SyncStats {
|
|
19
|
+
copied: number;
|
|
20
|
+
skipped: number;
|
|
21
|
+
deleted: number;
|
|
22
|
+
errors: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SyncConfig {
|
|
26
|
+
fedoc: string;
|
|
27
|
+
tcsdd: string;
|
|
28
|
+
relativeDir: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ==================== 同步配置 ====================
|
|
32
|
+
|
|
33
|
+
// 默认配置:src/staticMeta 到目标项目
|
|
34
|
+
const defaultConfig: SyncConfig = {
|
|
35
|
+
fedoc: '/Users/admin/Documents/proj/tech/hk-fe-doc/src',
|
|
36
|
+
tcsdd: '/Users/admin/Documents/tc/tcsdd/src',
|
|
37
|
+
relativeDir: 'staticMeta',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ==================== 同步函数 ====================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 同步单个文件:根据内容和修改时间决定是否复制
|
|
44
|
+
*/
|
|
45
|
+
async function syncFile(
|
|
46
|
+
srcPath: string,
|
|
47
|
+
destPath: string,
|
|
48
|
+
stats: SyncStats,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
let shouldCopy = false;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// 检查目标文件是否存在
|
|
54
|
+
const destExists = await fs
|
|
55
|
+
.access(destPath)
|
|
56
|
+
.then(() => true)
|
|
57
|
+
.catch(() => false);
|
|
58
|
+
|
|
59
|
+
if (!destExists) {
|
|
60
|
+
// 目标文件不存在,直接复制
|
|
61
|
+
shouldCopy = true;
|
|
62
|
+
} else {
|
|
63
|
+
// 目标文件存在,比较内容和修改时间
|
|
64
|
+
const [srcStat, destStat, srcContent, destContent]
|
|
65
|
+
= await Promise.all([
|
|
66
|
+
fs.stat(srcPath),
|
|
67
|
+
fs.stat(destPath),
|
|
68
|
+
fs.readFile(srcPath),
|
|
69
|
+
fs.readFile(destPath),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// 先比较内容
|
|
73
|
+
if (!srcContent.equals(destContent)) {
|
|
74
|
+
shouldCopy = true;
|
|
75
|
+
} else if (srcStat.mtime > destStat.mtime) {
|
|
76
|
+
// 内容相同但源文件更新,也复制
|
|
77
|
+
shouldCopy = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (shouldCopy) {
|
|
82
|
+
await fs.copyFile(srcPath, destPath);
|
|
83
|
+
stats.copied++;
|
|
84
|
+
} else {
|
|
85
|
+
stats.skipped++;
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
stats.errors++;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取目录中所有文件的相对路径集合
|
|
95
|
+
*/
|
|
96
|
+
async function getFileSet(
|
|
97
|
+
dirPath: string,
|
|
98
|
+
basePath: string = dirPath,
|
|
99
|
+
): Promise<Set<string>> {
|
|
100
|
+
const fileSet = new Set<string>();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
104
|
+
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
107
|
+
const relativePath = path.relative(basePath, fullPath);
|
|
108
|
+
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
// 递归获取子目录中的文件
|
|
111
|
+
const subFiles = await getFileSet(fullPath, basePath);
|
|
112
|
+
subFiles.forEach((file) => fileSet.add(file));
|
|
113
|
+
} else if (entry.isFile()) {
|
|
114
|
+
if (!fullPath.endsWith('/dep.ts')) {
|
|
115
|
+
fileSet.add(relativePath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// 目录可能不存在,返回空集合
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return fileSet;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 删除目标目录中多余的文件和目录
|
|
128
|
+
*/
|
|
129
|
+
async function cleanupExtraFiles(
|
|
130
|
+
srcDir: string,
|
|
131
|
+
destDir: string,
|
|
132
|
+
stats: SyncStats,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
const srcFileSet = await getFileSet(srcDir);
|
|
136
|
+
const destFileSet = await getFileSet(destDir);
|
|
137
|
+
|
|
138
|
+
// 找出目标中有但源中没有的文件
|
|
139
|
+
const filesToDelete: string[] = [];
|
|
140
|
+
destFileSet.forEach((file) => {
|
|
141
|
+
if (!srcFileSet.has(file)) {
|
|
142
|
+
filesToDelete.push(file);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 删除多余的文件(从最深的路径开始删除,避免文件夹不为空的问题)
|
|
147
|
+
const sortedFiles = filesToDelete.sort((a, b) => b.localeCompare(a));
|
|
148
|
+
|
|
149
|
+
for (const file of sortedFiles) {
|
|
150
|
+
const fullPath = path.join(destDir, file);
|
|
151
|
+
try {
|
|
152
|
+
const stat = await fs.stat(fullPath);
|
|
153
|
+
if (stat.isDirectory()) {
|
|
154
|
+
await fs.rm(fullPath, { recursive: true, force: true });
|
|
155
|
+
} else {
|
|
156
|
+
await fs.unlink(fullPath);
|
|
157
|
+
}
|
|
158
|
+
stats.deleted++;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
fileId('deleteFileFailed').error(`Failed to delete ${fullPath}: ${err}`);
|
|
161
|
+
stats.errors++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
fileId('cleanupFailed').error(`Failed to cleanup extra files: ${err}`);
|
|
166
|
+
stats.errors++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 递归同步目录树(使用 Eff 处理取消信号)
|
|
172
|
+
*/
|
|
173
|
+
function syncDirectory(
|
|
174
|
+
srcPath: string,
|
|
175
|
+
destPath: string,
|
|
176
|
+
stats: SyncStats,
|
|
177
|
+
signal: AbortSignal,
|
|
178
|
+
): ResultAsync<void, Error> {
|
|
179
|
+
return Eff.create(
|
|
180
|
+
async ({ signal: innerSignal }) => {
|
|
181
|
+
if (innerSignal.aborted) return err(new Error('操作已取消'));
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// 确保源目录存在
|
|
185
|
+
await fs.access(srcPath);
|
|
186
|
+
|
|
187
|
+
// 确保目标目录存在
|
|
188
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
189
|
+
|
|
190
|
+
// 读取源目录内容
|
|
191
|
+
const entries = await fs.readdir(srcPath, { withFileTypes: true });
|
|
192
|
+
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (innerSignal.aborted) return err(new Error('操作已取消'));
|
|
195
|
+
|
|
196
|
+
const srcEntryPath = path.join(srcPath, entry.name);
|
|
197
|
+
const destEntryPath = path.join(destPath, entry.name);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
if (entry.isDirectory()) {
|
|
201
|
+
// 递归同步子目录
|
|
202
|
+
const dirResult = await syncDirectory(
|
|
203
|
+
srcEntryPath,
|
|
204
|
+
destEntryPath,
|
|
205
|
+
stats,
|
|
206
|
+
innerSignal,
|
|
207
|
+
);
|
|
208
|
+
if (dirResult.isErr()) {
|
|
209
|
+
return err(dirResult.error);
|
|
210
|
+
}
|
|
211
|
+
} else if (entry.isFile()) {
|
|
212
|
+
// 同步文件
|
|
213
|
+
if (srcEntryPath.endsWith('/dep.ts')) continue; // 跳过 dep.ts 文件
|
|
214
|
+
await syncFile(srcEntryPath, destEntryPath, stats);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
stats.errors++;
|
|
218
|
+
fileId('syncEntryFailed').error(
|
|
219
|
+
`Failed to sync ${entry.isDirectory() ? 'directory' : 'file'} ${srcEntryPath}: ${err}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return ok(undefined);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return err(new Error(`同步失败: ${error}`));
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
signal,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 执行目录同步:返回统计信息(已复制、已跳过、已删除、错误数)
|
|
235
|
+
*/
|
|
236
|
+
function sync(config: SyncConfig, from: 'fedoc' | 'tcsdd'): ResultAsync<SyncStats, Error> {
|
|
237
|
+
return Eff.root(async ({ signal }) => {
|
|
238
|
+
if (signal.aborted) return err(new Error('操作已取消'));
|
|
239
|
+
|
|
240
|
+
const stats: SyncStats = {
|
|
241
|
+
copied: 0,
|
|
242
|
+
skipped: 0,
|
|
243
|
+
deleted: 0,
|
|
244
|
+
errors: 0,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const to = from === 'fedoc' ? 'tcsdd' : 'fedoc';
|
|
248
|
+
|
|
249
|
+
const srcDir = path.join(config[from], config.relativeDir);
|
|
250
|
+
const destDir = path.join(config[to], config.relativeDir);
|
|
251
|
+
fileId('syncStart').info(
|
|
252
|
+
`开始同步: ${srcDir} -> ${destDir}`,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const result = await syncDirectory(srcDir, destDir, stats, signal);
|
|
256
|
+
|
|
257
|
+
if (result.isErr()) {
|
|
258
|
+
stats.errors++;
|
|
259
|
+
fileId('syncDirectoryFailed').error(`Failed to sync directory: ${result.error}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 清理目标目录中多余的文件(源目录中已删除的文件)
|
|
263
|
+
if (signal.aborted) return ok(stats);
|
|
264
|
+
await cleanupExtraFiles(srcDir, destDir, stats);
|
|
265
|
+
|
|
266
|
+
return ok(stats);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 同步 staticMeta 目录:支持在两个项目之间双向同步
|
|
272
|
+
*/
|
|
273
|
+
export const tz = (tzCtx: { selection: ['fedoc' | 'tcsdd'] }) => {
|
|
274
|
+
const from = tzCtx.selection[0];
|
|
275
|
+
if (from !== 'fedoc' && from !== 'tcsdd') return;
|
|
276
|
+
return Eff.root(async ({ signal }) => {
|
|
277
|
+
if (signal.aborted) return err(new Error('操作已取消'));
|
|
278
|
+
|
|
279
|
+
const config = defaultConfig;
|
|
280
|
+
|
|
281
|
+
const result = await sync(config, from);
|
|
282
|
+
|
|
283
|
+
if (result.isErr()) {
|
|
284
|
+
return err(result.error);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const stats = result.value;
|
|
288
|
+
fileId('syncComplete').info('同步完成:');
|
|
289
|
+
fileId('syncStatsCopied').info(` - 已复制: ${stats.copied} 个文件`);
|
|
290
|
+
fileId('syncStatsSkipped').info(` - 已跳过: ${stats.skipped} 个文件`);
|
|
291
|
+
fileId('syncStatsDeleted').info(` - 已删除: ${stats.deleted} 个文件`);
|
|
292
|
+
fileId('syncStatsErrors').info(` - 错误: ${stats.errors} 个`);
|
|
293
|
+
|
|
294
|
+
return ok(stats);
|
|
295
|
+
});
|
|
296
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL 查询参数操作工具:构造、改写、合并查询参数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 基于 base 和相对路径构造完整 URL,支持查询参数和 hash
|
|
7
|
+
*/
|
|
8
|
+
export const buildUrl = (
|
|
9
|
+
base: string,
|
|
10
|
+
path: string,
|
|
11
|
+
params: Record<string, any> = {},
|
|
12
|
+
hash?: string,
|
|
13
|
+
) => {
|
|
14
|
+
const url = new URL(path, base); // path 可为相对/绝对
|
|
15
|
+
const sp = url.searchParams;
|
|
16
|
+
|
|
17
|
+
// 删除/增查参数 (会自动编码)
|
|
18
|
+
for (const [k, v] of Object.entries(params)) {
|
|
19
|
+
if (v === undefined || v === null) {
|
|
20
|
+
sp.delete(k);
|
|
21
|
+
} else if (Array.isArray(v)) {
|
|
22
|
+
sp.delete(k);
|
|
23
|
+
v.forEach((item: any) => sp.append(k, String(item)));
|
|
24
|
+
} else {
|
|
25
|
+
sp.set(k, String(v));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (hash !== undefined) url.hash = hash; // 哈希 "#top" (可能不带)
|
|
30
|
+
return url.toString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 改写现有 URL 的查询参数和 hash(支持设置、追加、删除参数)
|
|
35
|
+
*/
|
|
36
|
+
export const rewriteUrl = (
|
|
37
|
+
input: string,
|
|
38
|
+
{
|
|
39
|
+
set = {},
|
|
40
|
+
append = {},
|
|
41
|
+
del = [],
|
|
42
|
+
hash,
|
|
43
|
+
}: {
|
|
44
|
+
set?: Record<string, any>;
|
|
45
|
+
append?: Record<string, any>;
|
|
46
|
+
del?: string[];
|
|
47
|
+
hash?: string;
|
|
48
|
+
} = {},
|
|
49
|
+
) => {
|
|
50
|
+
const url = new URL(input);
|
|
51
|
+
const sp = url.searchParams;
|
|
52
|
+
|
|
53
|
+
// 设置参数
|
|
54
|
+
for (const [k, v] of Object.entries(set)) {
|
|
55
|
+
if (v === undefined || v === null) sp.delete(k);
|
|
56
|
+
else sp.set(k, String(v));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 追加参数
|
|
60
|
+
for (const [k, v] of Object.entries(append)) {
|
|
61
|
+
if (Array.isArray(v)) v.forEach((item: any) => sp.append(k, String(item)));
|
|
62
|
+
else sp.append(k, String(v));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// del: 删除参数
|
|
66
|
+
del.forEach((k) => sp.delete(k));
|
|
67
|
+
|
|
68
|
+
if (hash !== undefined) url.hash = hash;
|
|
69
|
+
return url.toString();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 合并查询参数字符串和参数对象
|
|
74
|
+
*/
|
|
75
|
+
export const mergeQueryString = (search: string, path: Record<string, any> = {}) => {
|
|
76
|
+
const sp = new URLSearchParams(search); // 符合 "?a=1" 或 "a=1"
|
|
77
|
+
for (const [k, v] of Object.entries(path)) {
|
|
78
|
+
if (v === undefined || v === null) sp.delete(k);
|
|
79
|
+
else sp.set(k, String(v));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const qs = sp.toString();
|
|
83
|
+
return qs ? `?${qs}` : '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// export const tz = async () => {
|
|
87
|
+
// const cons = console;
|
|
88
|
+
// // 示例
|
|
89
|
+
// cons.log(
|
|
90
|
+
// buildUrl(
|
|
91
|
+
// 'https://example.com/app/',
|
|
92
|
+
// './user/profile',
|
|
93
|
+
// { a: '中文 空格', page: 2, tag: ['a', 'b'], empty: null },
|
|
94
|
+
// 'section-1',
|
|
95
|
+
// ),
|
|
96
|
+
// );
|
|
97
|
+
// // => https://example.com/app/user/profile?q=%E4%B8%AD%E6%96%87+%E7%A9%BA%E6%A0%BC%E6%A0%BCs%263%3D4
|
|
98
|
+
// // 示例:直接 token 匹配、追加 tag, 删除 debug
|
|
99
|
+
// cons.log(
|
|
100
|
+
// rewriteUrl('https://a.com/?token=old&debug=1#x', {
|
|
101
|
+
// set: { token: 'new' },
|
|
102
|
+
// append: { tag: ['x', 'y'] },
|
|
103
|
+
// del: ['debug'],
|
|
104
|
+
// }),
|
|
105
|
+
// );
|
|
106
|
+
// // => https://a.com/?token=new&tag=x&tag=y#x
|
|
107
|
+
// // 示例:合并查询参数
|
|
108
|
+
// cons.log(mergeQueryString('?a=1&b=2', { b: 'x', c: 3, a: null }));
|
|
109
|
+
// // => ?b=x&c=3
|
|
110
|
+
// };
|
package/tinypool.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Tinypool,
|
|
3
|
+
isTransferable,
|
|
4
|
+
isMovable,
|
|
5
|
+
markMovable,
|
|
6
|
+
isTaskQueue,
|
|
7
|
+
kTransferable,
|
|
8
|
+
kValue,
|
|
9
|
+
kQueueOptions,
|
|
10
|
+
kRequestCountField,
|
|
11
|
+
kResponseCountField,
|
|
12
|
+
kFieldCount,
|
|
13
|
+
} from "tinypool"
|
|
14
|
+
export type {
|
|
15
|
+
Options,
|
|
16
|
+
Task,
|
|
17
|
+
TaskQueue,
|
|
18
|
+
Transferable,
|
|
19
|
+
TinypoolChannel,
|
|
20
|
+
TinypoolWorker,
|
|
21
|
+
TinypoolData,
|
|
22
|
+
TinypoolPrivateData,
|
|
23
|
+
TinypoolWorkerMessage,
|
|
24
|
+
StartupMessage,
|
|
25
|
+
RequestMessage,
|
|
26
|
+
ResponseMessage,
|
|
27
|
+
} from "tinypool"
|