tcdona_unilib 1.0.1 → 1.0.3

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.
@@ -0,0 +1,374 @@
1
+ /**
2
+ * 自动更新所有 fileInit() 调用的路径参数为正确的相对路径
3
+ */
4
+
5
+ import { fileURLToPath } from "node:url"
6
+ import { err, ok } from "neverthrow"
7
+ import type { EffContext } from "./eff.js"
8
+ import { Eff } from "./eff.js"
9
+ import type { Result } from "neverthrow"
10
+ import type { SgNode } from "@ast-grep/napi"
11
+ import { fileInit } from "./enum.api.js"
12
+ import {
13
+ extractArgumentValue,
14
+ findFunctionCalls,
15
+ hasImport,
16
+ replaceRanges,
17
+ } from "./ast.js"
18
+ import {
19
+ getRelativePath,
20
+ parseFileAsync,
21
+ readFileAsync,
22
+ scanTypeScriptFilesAsync,
23
+ writeFileAsync,
24
+ } from "./ast.scan.js"
25
+
26
+ type NonEmptyArray<T> = readonly [T, ...T[]]
27
+
28
+ // ==================== 日志配置 ====================
29
+
30
+ const fileId = fileInit("staticMeta/idupdate.ts")
31
+
32
+ // ==================== 类型定义 ====================
33
+
34
+ type UpdateError =
35
+ | { type: "VALIDATION_FILENAME_FAILED"; message: string }
36
+ | { type: "FILE_ERROR"; message: string; filePath: string }
37
+
38
+ interface StaticMetaCallInfo {
39
+ startOffset: number
40
+ endOffset: number
41
+ currentValue: string | null
42
+ }
43
+
44
+ interface InvalidCall {
45
+ reason: "NO_ARGUMENT" | "VARIABLE_ARGUMENT" | "TEMPLATE_STRING"
46
+ line: number
47
+ text: string
48
+ }
49
+
50
+ // ==================== 初始化 ====================
51
+
52
+ const __filename = fileURLToPath(import.meta.url)
53
+
54
+ // ==================== 验证 ====================
55
+
56
+ function validate(): {
57
+ isErr: () => boolean
58
+ isOk: () => boolean
59
+ value?: void
60
+ error?: UpdateError
61
+ } {
62
+ // 验证 staticMeta 功能
63
+ const currentFileAbsPath = fileURLToPath(import.meta.url)
64
+ const expectedRelativePath = getRelativePath(currentFileAbsPath)
65
+
66
+ const conf = fileId("validate1").conf
67
+ if (conf.srcFile !== expectedRelativePath) {
68
+ return err({
69
+ type: "VALIDATION_FILENAME_FAILED",
70
+ message: `__filename 不正确:\n 期望: ${expectedRelativePath}\n 实际: ${conf.srcFile}`,
71
+ })
72
+ }
73
+
74
+ return ok(undefined)
75
+ }
76
+
77
+ // ==================== 文件处理 ====================
78
+
79
+ /**
80
+ * 找出文件中所有 fileInit 调用(有效和无效)
81
+ */
82
+ function findFileInitCalls(
83
+ root: SgNode
84
+ ): [StaticMetaCallInfo[], InvalidCall[]] {
85
+ const validCalls: StaticMetaCallInfo[] = []
86
+ const invalidCalls: InvalidCall[] = []
87
+
88
+ const allCalls = findFunctionCalls(root, "fileInit")
89
+
90
+ for (const call of allCalls) {
91
+ const children = call.children()
92
+ if (children.length < 2) continue
93
+
94
+ const args = children[1]
95
+
96
+ if (args?.kind() !== "arguments") continue
97
+
98
+ const value = extractArgumentValue(args)
99
+ const range = call.range()
100
+
101
+ if (value === null) {
102
+ const argText = args.text()
103
+ let reason: InvalidCall["reason"] = "NO_ARGUMENT"
104
+
105
+ if (argText.includes("`")) {
106
+ reason = "TEMPLATE_STRING"
107
+ } else if (
108
+ argText.length > 2 &&
109
+ !argText.includes('"') &&
110
+ !argText.includes("'")
111
+ ) {
112
+ reason = "VARIABLE_ARGUMENT"
113
+ }
114
+
115
+ invalidCalls.push({
116
+ reason,
117
+ line: range.start.line,
118
+ text: call.text(),
119
+ })
120
+ continue
121
+ }
122
+
123
+ validCalls.push({
124
+ startOffset: range.start.index,
125
+ endOffset: range.end.index,
126
+ currentValue: value,
127
+ })
128
+ }
129
+
130
+ return [validCalls, invalidCalls]
131
+ }
132
+
133
+ /**
134
+ * 更新文件中所有 fileInit 调用的路径参数
135
+ */
136
+ async function updateStaticMeta(
137
+ filePath: string,
138
+ content: string,
139
+ root: SgNode,
140
+ dryRun: boolean,
141
+ signal: AbortSignal
142
+ ): Promise<{ isErr: boolean; error?: UpdateError; value?: boolean }> {
143
+ // 计算正确路径
144
+ const relativePath = getRelativePath(filePath)
145
+
146
+ // 检测导入 fileInit
147
+ if (!hasImport(root, "fileInit")) {
148
+ return { isErr: false, value: false }
149
+ }
150
+
151
+ // 查找 fileInit 调用
152
+ const [validCalls, invalidCalls] = findFileInitCalls(root)
153
+
154
+ // 记录无效调用
155
+ if (invalidCalls.length > 0) {
156
+ fileId("invalidFileInitCalls").warn(
157
+ `⚠️ ${filePath} 包含无效的 fileInit 调用:`
158
+ )
159
+ for (const invalid of invalidCalls) {
160
+ fileId("invalidFileInitDetail").warn(
161
+ ` L${invalid.line} [${invalid.reason}]: ${invalid.text}`
162
+ )
163
+ }
164
+ }
165
+
166
+ // 如果没有有效调用则跳过
167
+ if (validCalls.length === 0) {
168
+ return { isErr: false, value: false }
169
+ }
170
+
171
+ // 替换内容
172
+ const updatedContent = replaceRanges(
173
+ content,
174
+ validCalls.map((call) => ({
175
+ startIndex: call.startOffset,
176
+ endIndex: call.endOffset,
177
+ replacement: `fileInit('${relativePath}')`,
178
+ }))
179
+ )
180
+
181
+ // 写入文件
182
+ if (!dryRun) {
183
+ const writeResult = await writeFileAsync(filePath, updatedContent, signal)
184
+ if (writeResult.isErr()) {
185
+ return {
186
+ isErr: true,
187
+ error: {
188
+ type: "FILE_ERROR",
189
+ message: `无法写入文件: ${writeResult.error.message}`,
190
+ filePath,
191
+ },
192
+ }
193
+ }
194
+ }
195
+
196
+ return { isErr: false, value: true }
197
+ }
198
+
199
+ // ==================== 更新文件执行器 ====================
200
+
201
+ /**
202
+ * 为单个文件创建更新任务 executor
203
+ */
204
+ function createUpdateExecutor(
205
+ file: string,
206
+ dryRun: boolean
207
+ ): (
208
+ ctx: EffContext
209
+ ) => Promise<Result<{ updated: boolean; file: string } | null, UpdateError>> {
210
+ return async ({ signal: innerSignal }: EffContext) => {
211
+ if (innerSignal.aborted) {
212
+ return err({ type: "FILE_ERROR", message: "任务已取消", filePath: file })
213
+ }
214
+
215
+ const readResult = await readFileAsync(file, innerSignal)
216
+ if (readResult.isErr()) {
217
+ return err({
218
+ type: "FILE_ERROR",
219
+ message: `无法读取文件: ${readResult.error.message}`,
220
+ filePath: file,
221
+ })
222
+ }
223
+
224
+ const parseResult = await parseFileAsync(file, innerSignal)
225
+ if (parseResult.isErr()) {
226
+ return err({
227
+ type: "FILE_ERROR",
228
+ message: `AST 解析失败: ${parseResult.error.message}`,
229
+ filePath: file,
230
+ })
231
+ }
232
+
233
+ const updateResult = await updateStaticMeta(
234
+ file,
235
+ readResult.value,
236
+ parseResult.value.root,
237
+ dryRun,
238
+ innerSignal
239
+ )
240
+ if (updateResult.isErr) {
241
+ return err(updateResult.error!)
242
+ }
243
+
244
+ return ok(updateResult.value ? { updated: true, file } : null)
245
+ }
246
+ }
247
+
248
+ // ==================== 结果处理与汇总 ====================
249
+
250
+ /**
251
+ * 统计、分类并输出更新结果
252
+ */
253
+ function collectAndPrintResults(
254
+ results: Result<{ updated: boolean; file: string } | null, UpdateError>[],
255
+ files: string[],
256
+ dryRun: boolean
257
+ ): { updatedFiles: string[]; errors: UpdateError[] } {
258
+ const updatedFiles: string[] = []
259
+ const skippedFiles: string[] = []
260
+ const errors: UpdateError[] = []
261
+
262
+ for (let i = 0; i < results.length; i++) {
263
+ const result = results[i]
264
+ const file = files[i]
265
+
266
+ if (result.isErr()) {
267
+ errors.push(result.error)
268
+ } else if (result.value?.updated) {
269
+ updatedFiles.push(getRelativePath(result.value.file))
270
+ fileId("updateProgress").info(` ✓ ${getRelativePath(result.value.file)}`)
271
+ } else {
272
+ skippedFiles.push(getRelativePath(file))
273
+ }
274
+ }
275
+
276
+ // 打印汇总
277
+ fileId("summaryHeaderLine1").info(`\n${"=".repeat(60)}`)
278
+ fileId("summaryHeaderLine2").info("更新结果汇总")
279
+ fileId("summaryHeaderLine3").info(`${"=".repeat(60)}`)
280
+
281
+ if (updatedFiles.length > 0) {
282
+ fileId("updateSuccess").info(
283
+ `\n✅ ${dryRun ? "将更新" : "已更新"} ${updatedFiles.length} 个文件`
284
+ )
285
+ }
286
+
287
+ if (skippedFiles.length > 0) {
288
+ fileId("updateSkipped").info(
289
+ `⊘ 已跳过 ${skippedFiles.length} 个文件 (无 fileInit 调用)`
290
+ )
291
+ }
292
+
293
+ if (errors.length > 0) {
294
+ fileId("updateFailed").error(`\n❌ 更新失败 ${errors.length} 个文件:`)
295
+ for (const error of errors) {
296
+ const filePath = error.type === "FILE_ERROR" ? error.filePath : "(验证)"
297
+ const message = error.message
298
+ fileId("updateErrorPath").error(` ${filePath}`)
299
+ fileId("updateErrorMessage").error(` └─ ${message}`)
300
+ }
301
+ }
302
+
303
+ fileId("summaryFooter").info(`${"=".repeat(60)}\n`)
304
+
305
+ return { updatedFiles, errors }
306
+ }
307
+
308
+ // ==================== 主函数 ====================
309
+
310
+ const main = () => {
311
+ return Eff.root(async ({ signal }) => {
312
+ if (signal.aborted) return err(new Error("操作已取消"))
313
+
314
+ const args = process.argv.slice(2)
315
+ const dryRun = args.includes("--dry-run") || args.includes("-n")
316
+
317
+ // 验证 staticMeta 功能
318
+ // const validationResult = validate();
319
+ // if (validationResult.isErr?.()) {
320
+ // fileId('validateFailed').error(
321
+ // '验证失败:',
322
+ // (validationResult as any).error.message,
323
+ // );
324
+ // // 下面的这个 as any 会让类型推断中断,从而无法保证,return 后面的所有语句的推导正确性,再一次证明了 as any 要少用
325
+ // return err((validationResult as any).error);
326
+ // }
327
+
328
+ // 扫描文件
329
+ const files = await scanTypeScriptFilesAsync(["**/staticMeta/idupdate.ts"])
330
+ fileId("updateStart").info(
331
+ `开始更新 fileInit... (共 ${files.length} 个文件)\n`
332
+ )
333
+
334
+ if (files.length === 0) {
335
+ return ok(undefined)
336
+ }
337
+
338
+ // 创建并行更新任务
339
+ type UpdateResult = { updated: boolean; file: string } | null
340
+ const executors = files.map((file) =>
341
+ createUpdateExecutor(file, dryRun)
342
+ ) as unknown as NonEmptyArray<
343
+ (ctx: EffContext) => Promise<Result<UpdateResult, UpdateError>>
344
+ >
345
+
346
+ // 执行并行更新
347
+ const allResults = await Eff.allSettled(executors, signal)
348
+ const results = allResults.isOk() ? allResults.value : []
349
+
350
+ // 统计结果并输出汇总
351
+ const { errors } = collectAndPrintResults(results, files, dryRun)
352
+
353
+ if (errors.length > 0) {
354
+ const error = errors[0]
355
+ const message =
356
+ error.type === "FILE_ERROR"
357
+ ? `${error.message} (${error.filePath})`
358
+ : error.message
359
+ return err(new Error(message))
360
+ }
361
+
362
+ return ok(undefined)
363
+ })
364
+ }
365
+
366
+ main().match(
367
+ () => {
368
+ /* success */
369
+ },
370
+ (err) => {
371
+ fileId("scriptFailed").error(`脚本执行失败: ${err.message ?? String(err)}`)
372
+ process.exit(1)
373
+ }
374
+ )
@@ -0,0 +1,21 @@
1
+ import { dirname, relative, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ /**
5
+ * 初始化模块的路径元信息(__filename, __dirname, __relative, resolve)
6
+ * @param metaUrl 模块 URL(import.meta.url)
7
+ * @param root 根目录路径
8
+ */
9
+ export function pathMeta(metaUrl: string, root: string) {
10
+ const __filename = fileURLToPath(metaUrl);
11
+ const __dirname = dirname(__filename);
12
+ const __rootname = resolve(root);
13
+
14
+ return {
15
+ __rootname,
16
+ __dirname,
17
+ __filename,
18
+ resolve: resolve.bind(null, __dirname),
19
+ __relative: relative(__rootname, __filename),
20
+ } as const;
21
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * 自动生成 package.json 中的 staticMeta 相关脚本配置
3
+ * 依赖 zx 库查找外层的 package.json 并生成正确的脚本条目
4
+ *
5
+ * 使用方式:
6
+ * bun run node_modules/tcdona_unilib/staticMeta/pkg.json.ts # 更新 package.json
7
+ * bun run node_modules/tcdona_unilib/staticMeta/pkg.json.ts --dry-run # 预览将要进行的更改
8
+ */
9
+
10
+ import { $ } from 'zx';
11
+ import { resolve } from 'node:path';
12
+ import { readFileSync, writeFileSync } from 'node:fs';
13
+ import { fileInit, TcErr } from './enum.api.js';
14
+
15
+ // ==================== 初始化 ====================
16
+
17
+ const fileId = fileInit('staticMeta/pkg.json.ts');
18
+
19
+ // ==================== 脚本配置定义 ====================
20
+
21
+ /**
22
+ * staticMeta 脚本配置映射表
23
+ * 这是唯一的配置源,所有脚本更新都基于这个定义
24
+ */
25
+ const STATIC_META_SCRIPTS = {
26
+ 'iduniq': 'bun run node_modules/tcdona_unilib/staticMeta/iduniq.ts',
27
+ 'idupdate': 'bun run node_modules/tcdona_unilib/staticMeta/idupdate.ts',
28
+ 'idupdate:dry': 'bun run node_modules/tcdona_unilib/staticMeta/idupdate.ts --dry-run',
29
+ } as const;
30
+
31
+ // ==================== 查找 package.json ====================
32
+
33
+ /**
34
+ * 从当前文件向上递归查找 package.json
35
+ * 优先使用 git 仓库根目录
36
+ */
37
+ async function findPackageJsonPath(): Promise<string> {
38
+ try {
39
+ const result = await $`git rev-parse --show-toplevel`;
40
+ const gitRoot = result.stdout.trim();
41
+ const pkgPath = resolve(gitRoot, 'package.json');
42
+ return pkgPath;
43
+ } catch (error) {
44
+ throw new TcErr({
45
+ fildId: fileId('findPkgPathFailed'),
46
+ message: '无法查找 package.json',
47
+ cause: error,
48
+ });
49
+ }
50
+ }
51
+
52
+ // ==================== 更新 package.json ====================
53
+
54
+ /**
55
+ * 更新 package.json 中的脚本配置
56
+ */
57
+ function updatePackageJson(pkgPath: string, dryRun: boolean = false): void {
58
+ try {
59
+ const content = readFileSync(pkgPath, 'utf-8');
60
+ const pkg = JSON.parse(content);
61
+
62
+ if (!pkg.scripts) {
63
+ pkg.scripts = {};
64
+ }
65
+
66
+ let updated = false;
67
+ const changes: { key: string; oldValue?: string; newValue: string }[] = [];
68
+
69
+ // 检查并收集需要更新的脚本
70
+ for (const [key, value] of Object.entries(STATIC_META_SCRIPTS)) {
71
+ const oldValue = pkg.scripts[key];
72
+ if (oldValue !== value) {
73
+ changes.push({ key, oldValue, newValue: value });
74
+ updated = true;
75
+ }
76
+ }
77
+
78
+ // 输出变更信息
79
+ if (changes.length > 0) {
80
+ fileId('detectChanges').info(`检测到 ${changes.length} 个需要更新的脚本:`);
81
+ for (const change of changes) {
82
+ if (change.oldValue) {
83
+ fileId('changeKey').info(` • ${change.key}`);
84
+ fileId('changeOldValue').warn(` - 旧值: ${change.oldValue}`);
85
+ fileId('changeNewValue').info(` + 新值: ${change.newValue}`);
86
+ } else {
87
+ fileId('addNewScript').info(` + ${change.key}: ${change.newValue}`);
88
+ }
89
+ }
90
+ }
91
+
92
+ // 写入文件
93
+ if (updated && !dryRun) {
94
+ for (const { key, newValue } of changes) {
95
+ pkg.scripts[key] = newValue;
96
+ }
97
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
98
+ fileId('updateSuccess').info(`\n✅ 已成功更新 ${pkgPath}`);
99
+ } else if (updated && dryRun) {
100
+ fileId('dryRunWarning').warn('\n⊘ [Dry Run] 将要更新以上脚本 (使用 bun run pkg:json 执行实际更新)');
101
+ } else if (!updated) {
102
+ fileId('alreadyLatest').info('\n✓ 所有脚本配置已是最新');
103
+ }
104
+ } catch (error) {
105
+ throw new TcErr({
106
+ fildId: fileId('updatePkgJsonFailed'),
107
+ pkgPath,
108
+ cause: error,
109
+ });
110
+ }
111
+ }
112
+
113
+ // ==================== 主函数 ====================
114
+
115
+ export const tz = async () => {
116
+ try {
117
+ const args = process.argv.slice(2);
118
+ const dryRun = args.includes('--dry-run') || args.includes('-n');
119
+
120
+ const modeLabel = dryRun ? '[预览]' : '[更新]';
121
+ fileId('startup').info(`${modeLabel} 生成 staticMeta 脚本配置\n`);
122
+
123
+ const pkgPath = await findPackageJsonPath();
124
+ fileId('foundPkgPath').info(`位置: ${pkgPath}\n`);
125
+
126
+ updatePackageJson(pkgPath, dryRun);
127
+ } catch (error) {
128
+ if (error instanceof TcErr) {
129
+ // TcErr 已经在构造器中输出了
130
+ process.exit(1);
131
+ }
132
+
133
+ throw new TcErr({
134
+ fildId: fileId('mainFailed'),
135
+ cause: error,
136
+ });
137
+ }
138
+ };
@@ -0,0 +1,14 @@
1
+ import { customAlphabet } from 'nanoid';
2
+
3
+ /**
4
+ * 生成 18 字符的唯一 ID(使用自定义字符集,排除易混淆字符)
5
+ */
6
+ export const tcNanoid = customAlphabet(
7
+ 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict',
8
+ 18,
9
+ );
10
+
11
+ export const lowerNanoid = customAlphabet(
12
+ 'useandom2619834075pxbfghjklqvwyzrict',
13
+ 18,
14
+ );